diff options
author | Servo WPT Sync <32481905+servo-wpt-sync@users.noreply.github.com> | 2024-06-16 00:41:02 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-16 04:41:02 +0000 |
commit | d439faf6fb0704a47e07004ccb745c4e00778420 (patch) | |
tree | 3463033e366a96a791255acb5a48f8001750947c | |
parent | 7982f0dc27cad98057a0a953cca76e2b86aa37ed (diff) | |
download | servo-d439faf6fb0704a47e07004ccb745c4e00778420.tar.gz servo-d439faf6fb0704a47e07004ccb745c4e00778420.zip |
Update web-platform-tests to revision b'ed9e9309618bdf76de06ff85757edbc8e1d7da82' (#32512)
Signed-off-by: WPT Sync Bot <josh+wptsync@joshmatthews.net>
1393 files changed, 71511 insertions, 47379 deletions
diff --git a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.any.js.ini b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.any.js.ini index 5622c8d4c19..1825bb55774 100644 --- a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.any.js.ini +++ b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.any.js.ini @@ -14,6 +14,15 @@ [FileReader interface: calling readAsBinaryString(Blob) on new FileReader() with too few arguments must throw TypeError] expected: FAIL + [Blob interface: operation bytes()] + expected: FAIL + + [Blob interface: new Blob(["TEST"\]) must inherit property "bytes()" with the proper type] + expected: FAIL + + [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "bytes()" with the proper type] + expected: FAIL + [idlharness.any.html] [Blob interface: operation text()] @@ -30,3 +39,12 @@ [FileReader interface: calling readAsBinaryString(Blob) on new FileReader() with too few arguments must throw TypeError] expected: FAIL + + [Blob interface: operation bytes()] + expected: FAIL + + [Blob interface: new Blob(["TEST"\]) must inherit property "bytes()" with the proper type] + expected: FAIL + + [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "bytes()" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.html.ini b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.html.ini index aa610053984..2f31759dca4 100644 --- a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.html.ini +++ b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.html.ini @@ -24,3 +24,5 @@ [Blob interface: operation arrayBuffer()] expected: FAIL + [Blob interface: operation bytes()] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.worker.js.ini b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.worker.js.ini index b47ec2ab313..a76b974ec4e 100644 --- a/tests/wpt/meta-legacy-layout/FileAPI/idlharness.worker.js.ini +++ b/tests/wpt/meta-legacy-layout/FileAPI/idlharness.worker.js.ini @@ -36,3 +36,5 @@ [Blob interface: operation arrayBuffer()] expected: FAIL + [Blob interface: operation bytes()] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini b/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini new file mode 100644 index 00000000000..1b96343c8a3 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini @@ -0,0 +1,2 @@ +[background-size-contain-svg-view.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini b/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini new file mode 100644 index 00000000000..bd7b19d36fd --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini @@ -0,0 +1,2 @@ +[background-size-cover-svg-view.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-color/animation/opacity-animation-ending-correctly-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-color/animation/opacity-animation-ending-correctly-002.html.ini new file mode 100644 index 00000000000..eb3cf41a070 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-color/animation/opacity-animation-ending-correctly-002.html.ini @@ -0,0 +1,2 @@ +[opacity-animation-ending-correctly-002.html] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-003.html.ini b/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-003.html.ini new file mode 100644 index 00000000000..0553b106708 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-003.html.ini @@ -0,0 +1,2 @@ +[font-variant-emoji-003.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-004.html.ini b/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-004.html.ini new file mode 100644 index 00000000000..61006a4ff60 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-fonts/font-variant-emoji-004.html.ini @@ -0,0 +1,2 @@ +[font-variant-emoji-004.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-001.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-001.tentative.html.ini deleted file mode 100644 index 25535f43bd2..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-001.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-001.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-004.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-004.tentative.html.ini deleted file mode 100644 index 079d68f9db0..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-004.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-004.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-010.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-010.tentative.html.ini deleted file mode 100644 index 197075b7e1a..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-010.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-010.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-015.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-015.tentative.html.ini deleted file mode 100644 index e397f59fad1..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-015.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-015.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-001.html.ini new file mode 100644 index 00000000000..039be87e913 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-001.html.ini @@ -0,0 +1,2 @@ +[line-clamp-001.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-004.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-004.html.ini new file mode 100644 index 00000000000..5ea4302a5de --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-004.html.ini @@ -0,0 +1,2 @@ +[line-clamp-004.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-005.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-005.tentative.html.ini index f7e5bd89633..f7e5bd89633 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-005.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-005.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-006.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-006.tentative.html.ini index a20fcc3b242..a20fcc3b242 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-006.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-006.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-007.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-007.tentative.html.ini index 650254666e8..650254666e8 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-007.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-007.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-008.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-008.tentative.html.ini index 01050849b00..01050849b00 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-008.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-008.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-009.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-009.tentative.html.ini index c5fde4b81a4..c5fde4b81a4 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-009.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-009.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-010.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-010.html.ini new file mode 100644 index 00000000000..ffb496aa0c0 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-010.html.ini @@ -0,0 +1,2 @@ +[line-clamp-010.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-011.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-011.tentative.html.ini index 801b5a840df..801b5a840df 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-011.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-011.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-012.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-012.tentative.html.ini index a7fd0303bee..a7fd0303bee 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-012.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-012.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-013.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-013.tentative.html.ini index 3f398e788d2..3f398e788d2 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-013.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-013.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-015.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-015.html.ini new file mode 100644 index 00000000000..0619b41ed9a --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-015.html.ini @@ -0,0 +1,2 @@ +[line-clamp-015.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-016.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-016.tentative.html.ini index 8c13ceb8691..8c13ceb8691 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-016.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-016.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-017.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-017.tentative.html.ini index e98e35fd17a..e98e35fd17a 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-017.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-017.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-019.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-019.tentative.html.ini index baf2cf8f33d..baf2cf8f33d 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-019.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-019.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-001.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html.ini index 0846054622b..0846054622b 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-001.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-002.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html.ini index 672bbf776fe..672bbf776fe 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-002.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-003.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html.ini index 8fdd292cb00..8fdd292cb00 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-003.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-004.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html.ini index 29770e6524a..29770e6524a 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-004.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-005.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html.ini index 7c7c26bce5c..7c7c26bce5c 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-005.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-009.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html.ini index 8e3126191e7..8e3126191e7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-009.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-010.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-010.tentative.html.ini index 1be7dc44f47..1be7dc44f47 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-010.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-010.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-011.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html.ini index 55e2d3799e9..55e2d3799e9 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-011.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-013.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html.ini index 080b13f055b..080b13f055b 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-013.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-014.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html.ini index 83b70019122..83b70019122 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-014.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-015.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html.ini index 96067a1553b..96067a1553b 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-015.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-016.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html.ini index bc8e676d3fb..bc8e676d3fb 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-auto-016.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-001.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html.ini index 96277819670..96277819670 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-001.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-002.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html.ini index 21efb29c1f9..21efb29c1f9 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-002.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-003.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html.ini index 189ac0f3620..189ac0f3620 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-003.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-004.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html.ini index 4e4d8534002..4e4d8534002 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-004.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-005.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html.ini index 4a2b7b2ba98..4a2b7b2ba98 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-005.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-006.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html.ini index bd697fabf3f..bd697fabf3f 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-006.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-007.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html.ini index b63c81cd54e..b63c81cd54e 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-007.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-008.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html.ini index 834446721a7..834446721a7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-008.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-009.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html.ini index 24f8b5bded3..24f8b5bded3 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-009.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-010.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html.ini index b4d85b57450..b4d85b57450 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-010.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-011.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html.ini index b399afd2326..b399afd2326 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-abspos-011.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-001.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html.ini index b3a7f12f4eb..b3a7f12f4eb 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-001.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-002.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html.ini index 73ed3ce5449..73ed3ce5449 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-002.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-003.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html.ini index daeb119383d..daeb119383d 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-003.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-004.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html.ini index ee7aa7cffa8..ee7aa7cffa8 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-004.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-005.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html.ini index 0321cf6b3d7..0321cf6b3d7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-005.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-006.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html.ini index 9cb5ff1b379..9cb5ff1b379 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-006.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-007.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html.ini index 547cac7a2ca..547cac7a2ca 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-007.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-008.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html.ini index 23716f1941f..23716f1941f 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-008.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-009.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html.ini index 1f92c2b17f5..1f92c2b17f5 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-009.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-010.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-010.tentative.html.ini index a47629e1f4c..a47629e1f4c 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp-with-floats-010.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/line-clamp-with-floats-010.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-005.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-005.html.ini index fd3b482cd4c..fd3b482cd4c 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-005.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-005.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-006.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-006.html.ini index 37a36c18ed0..37a36c18ed0 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-006.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-006.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-007.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-007.html.ini index ee3feba6326..ee3feba6326 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-007.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-007.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-008.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-008.html.ini index d7d111177a0..d7d111177a0 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-008.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-008.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-009.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-009.html.ini index 2eeee791cf0..2eeee791cf0 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-009.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-009.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-010.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-010.html.ini index eb78fb115b3..eb78fb115b3 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-010.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-010.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-011.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-011.html.ini index 9ffffc40f10..9ffffc40f10 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-011.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-011.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-012.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-012.html.ini index d65978f7379..d65978f7379 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-012.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-012.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-013.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-013.html.ini index 80db2cbf4b0..80db2cbf4b0 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-013.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-013.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-014.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-014.html.ini index 10e4bd44fd1..10e4bd44fd1 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-014.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-014.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-016.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-016.html.ini index 7787a8caf58..7787a8caf58 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-016.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-016.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-017.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-017.html.ini index 30fed79972d..30fed79972d 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-017.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-017.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-020.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-020.html.ini index 920a915cbfd..920a915cbfd 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-020.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-020.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-021.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-021.html.ini index d4c6b4fa7ee..d4c6b4fa7ee 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-021.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-021.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-024.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-024.html.ini index 7958cc24372..7958cc24372 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-024.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-024.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-025.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-025.html.ini index f882cda18a2..f882cda18a2 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-025.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-025.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-027.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-027.html.ini index 0768495e834..0768495e834 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-027.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-027.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-030.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-030.html.ini index 15501f962a9..15501f962a9 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-030.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-030.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-031.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-031.html.ini index b676c4ceb75..b676c4ceb75 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-031.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-031.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-032.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-032.html.ini index 46b3b9509f5..46b3b9509f5 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-032.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-032.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-035.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-035.html.ini index fd300a1ade2..fd300a1ade2 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-035.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-035.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-036.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-036.html.ini index f2716aacf89..f2716aacf89 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-036.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-036.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-037.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-037.html.ini index ea4c9b1cda3..ea4c9b1cda3 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-037.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-037.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-040.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-040.html.ini index b3a2caf391e..b3a2caf391e 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-040.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-040.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-043.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-043.html.ini index 85656176cc9..85656176cc9 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-043.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-043.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html.ini index aacd5624a5d..aacd5624a5d 100644 --- a/tests/wpt/meta-legacy-layout/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/first-letter-width-2.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/first-letter-width-2.html.ini new file mode 100644 index 00000000000..4ecd64faef3 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/first-letter-width-2.html.ini @@ -0,0 +1,2 @@ +[first-letter-width-2.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-sizing/dynamic-available-size-iframe.html.ini b/tests/wpt/meta-legacy-layout/css/css-sizing/dynamic-available-size-iframe.html.ini deleted file mode 100644 index 35256741d38..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-sizing/dynamic-available-size-iframe.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[dynamic-available-size-iframe.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-001.html.ini new file mode 100644 index 00000000000..68f0fa04c80 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-001.html.ini @@ -0,0 +1,2 @@ +[visibility-collapse-border-spacing-001.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-002.html.ini new file mode 100644 index 00000000000..a5a39548b51 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing-002.html.ini @@ -0,0 +1,2 @@ +[visibility-collapse-border-spacing-002.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing.html.ini b/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing.html.ini deleted file mode 100644 index 43b3c65884f..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-tables/visibility-collapse-border-spacing.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[visibility-collapse-border-spacing.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-transforms/transform-with-sign-function.html.ini b/tests/wpt/meta-legacy-layout/css/css-transforms/transform-with-sign-function.html.ini index acc2f9ef2d3..a4c4b9fa419 100644 --- a/tests/wpt/meta-legacy-layout/css/css-transforms/transform-with-sign-function.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-transforms/transform-with-sign-function.html.ini @@ -28,3 +28,6 @@ [calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) should be used-value-equivalent to 2 2 2] expected: FAIL + + [calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2deg) should be used-value-equivalent to 2 2 2 2deg] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-values/rlh-unit-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-values/rlh-unit-001.html.ini new file mode 100644 index 00000000000..60dbcb52aed --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-values/rlh-unit-001.html.ini @@ -0,0 +1,2 @@ +[rlh-unit-001.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/cssom-view/idlharness.html.ini b/tests/wpt/meta-legacy-layout/css/cssom-view/idlharness.html.ini index 5bdc2305057..1518e58094c 100644 --- a/tests/wpt/meta-legacy-layout/css/cssom-view/idlharness.html.ini +++ b/tests/wpt/meta-legacy-layout/css/cssom-view/idlharness.html.ini @@ -604,3 +604,12 @@ [Document interface: calling caretPositionFromPoint(double, double, ShadowRoot...) on document with too few arguments must throw TypeError] expected: FAIL + + [Document interface: operation caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)] + expected: FAIL + + [Document interface: document must inherit property "caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)" with the proper type] + expected: FAIL + + [Document interface: calling caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions) on document with too few arguments must throw TypeError] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/cssom-view/smooth-scroll-nonstop.html.ini b/tests/wpt/meta-legacy-layout/css/cssom-view/smooth-scroll-nonstop.html.ini new file mode 100644 index 00000000000..fb43d2b8fa1 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/cssom-view/smooth-scroll-nonstop.html.ini @@ -0,0 +1,4 @@ +[smooth-scroll-nonstop.html] + expected: TIMEOUT + [noop scrollIntoView doesn't interrupt ongoing smooth scroll.] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/dom/events/EventTarget-constructible.any.js.ini b/tests/wpt/meta-legacy-layout/dom/events/EventTarget-constructible.any.js.ini new file mode 100644 index 00000000000..dd3d602fbb0 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/events/EventTarget-constructible.any.js.ini @@ -0,0 +1,8 @@ +[EventTarget-constructible.any.worker.html] + [A constructed EventTarget implements dispatch correctly] + expected: FAIL + + +[EventTarget-constructible.any.html] + [A constructed EventTarget implements dispatch correctly] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini b/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini index 31690f3ffdc..3292b652dbf 100644 --- a/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini @@ -28,3 +28,6 @@ [Basic AttributePart cloning with values] expected: FAIL + + [Basic AttributePart parsing with multiple attributes] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini b/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini deleted file mode 100644 index 3982572f016..00000000000 --- a/tests/wpt/meta-legacy-layout/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[basic-dom-part-declarative-pi-syntax.tentative.html] - [Basic declarative DOM Parts (Main Document)] - expected: FAIL - - [Basic declarative DOM Parts (Template)] - expected: FAIL - - [Basic declarative DOM Parts (Clone)] - expected: FAIL - - [Basic declarative DOM Parts (PartClone)] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/fetch/api/basic/keepalive.any.js.ini b/tests/wpt/meta-legacy-layout/fetch/api/basic/keepalive.any.js.ini index 9aa4b06752c..2ce2d784ab5 100644 --- a/tests/wpt/meta-legacy-layout/fetch/api/basic/keepalive.any.js.ini +++ b/tests/wpt/meta-legacy-layout/fetch/api/basic/keepalive.any.js.ini @@ -1,19 +1,15 @@ [keepalive.any.html] - expected: TIMEOUT [keepalive in onunload in nested frame in another window] expected: FAIL - [[keepalive\] simple GET request on 'pagehide' [no payload\]; setting up] - expected: TIMEOUT - - [[keepalive\] simple GET request on 'unload' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple GET request on 'unload' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'load' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple GET request on 'pagehide' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'pagehide' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple POST request on 'unload' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'unload' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple POST request on 'pagehide' [no payload\]] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/browsers/history/the-history-interface/traverse_the_history_4.html.ini b/tests/wpt/meta-legacy-layout/html/browsers/history/the-history-interface/traverse_the_history_4.html.ini new file mode 100644 index 00000000000..d6188c03424 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/browsers/history/the-history-interface/traverse_the_history_4.html.ini @@ -0,0 +1,3 @@ +[traverse_the_history_4.html] + [Multiple history traversals, last would be aborted] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini new file mode 100644 index 00000000000..18cea856e89 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini new file mode 100644 index 00000000000..4fa5266a456 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.shadow.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini new file mode 100644 index 00000000000..c7203b17045 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.shadow.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini new file mode 100644 index 00000000000..61941d9bc47 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini new file mode 100644 index 00000000000..c95e1c74724 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.with-render-states-and-filter.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini new file mode 100644 index 00000000000..8cbdaa1ebe8 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini b/tests/wpt/meta-legacy-layout/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini index 8b8af2b9c2e..2ef0896e3b3 100644 --- a/tests/wpt/meta-legacy-layout/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini +++ b/tests/wpt/meta-legacy-layout/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini @@ -1,3 +1,4 @@ [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-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 68203d2a082..7df8f9458e9 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: CRASH [Check that popups from a sandboxed iframe escape the sandbox if\n allow-popups-to-escape-sandbox is used] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini new file mode 100644 index 00000000000..05aa7ddc17f --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini @@ -0,0 +1,3 @@ +[option-computed-style.tentative.html] + [appearance:base-select options should have a checkmark with empty alt text.] + 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 17abd82b61c..6d63250a9fe 100644 --- a/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini +++ b/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini @@ -55,3 +55,12 @@ [ChapterInformation interface: attribute artwork] expected: FAIL + + [MediaSession interface: operation setScreenshareActive(boolean)] + expected: FAIL + + [MediaSession interface: navigator.mediaSession must inherit property "setScreenshareActive(boolean)" with the proper type] + expected: FAIL + + [MediaSession interface: calling setScreenshareActive(boolean) on navigator.mediaSession with too few arguments must throw TypeError] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/resource-timing/content-type.html.ini b/tests/wpt/meta-legacy-layout/resource-timing/content-type.html.ini index d1f4eb3ab5d..ec8afaefc79 100644 --- a/tests/wpt/meta-legacy-layout/resource-timing/content-type.html.ini +++ b/tests/wpt/meta-legacy-layout/resource-timing/content-type.html.ini @@ -80,3 +80,6 @@ [This test validates the content-type of resources. 26] expected: NOTRUN + + [This test validates the content-type of resources. 27] + expected: NOTRUN diff --git a/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.html.ini b/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.html.ini index 12581463c49..86a2572b9b8 100644 --- a/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.html.ini +++ b/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.html.ini @@ -68,6 +68,3 @@ [PerformanceEntry has correct protocol attribute (xmlhttprequest)] expected: FAIL - - [PerformanceEntry has correct name, initiatorType, startTime, and duration (img)] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.https.html.ini b/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.https.html.ini index 32d149c8ae9..54e3ef35665 100644 --- a/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.https.html.ini +++ b/tests/wpt/meta-legacy-layout/resource-timing/test_resource_timing.https.html.ini @@ -68,6 +68,3 @@ [PerformanceEntry has correct name, initiatorType, startTime, and duration (link)] expected: NOTRUN - - [PerformanceEntry has correct name, initiatorType, startTime, and duration (img)] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/url/IdnaTestV2.window.js.ini b/tests/wpt/meta-legacy-layout/url/IdnaTestV2.window.js.ini deleted file mode 100644 index 45667b117ed..00000000000 --- a/tests/wpt/meta-legacy-layout/url/IdnaTestV2.window.js.ini +++ /dev/null @@ -1,522 +0,0 @@ -[IdnaTestV2.window.html] - [ToASCII("ab") C1] - expected: FAIL - - [ToASCII("AB") C1] - expected: FAIL - - [ToASCII("Ab") C1] - expected: FAIL - - [ToASCII("xn--ab-j1t") C1] - expected: FAIL - - [ToASCII("ab") C2] - expected: FAIL - - [ToASCII("AB") C2] - expected: FAIL - - [ToASCII("Ab") C2] - expected: FAIL - - [ToASCII("xn--ab-m1t") C2] - expected: FAIL - - [ToASCII("1.aßbcßßßßdςσßßßßßßßßeßßßßßßßßßßxßßßßßßßßßßyßßßßßßßß̂ßz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.ASSBCSSSSSSSSDΣΣSSSSSSSSSSSSSSSSESSSSSSSSSSSSSSSSSSSSXSSSSSSSSSSSSSSSSSSSSYSSSSSSSSSSSSSSSŜSSZ") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.ASSBCSSSSSSSSDΣΣSSSSSSSSSSSSSSSSESSSSSSSSSSSSSSSSSSSSXSSSSSSSSSSSSSSSSSSSSYSSSSSSSSSSSSSSSŜSSZ") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.assbcssssssssdσσssssssssssssssssessssssssssssssssssssxssssssssssssssssssssysssssssssssssssŝssz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.assbcssssssssdσσssssssssssssssssessssssssssssssssssssxssssssssssssssssssssysssssssssssssssŝssz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.Assbcssssssssdσσssssssssssssssssessssssssssssssssssssxssssssssssssssssssssysssssssssssssssŝssz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.Assbcssssssssdσσssssssssssssssssessssssssssssssssssssxssssssssssssssssssssysssssssssssssssŝssz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.xn--assbcssssssssdssssssssssssssssessssssssssssssssssssxssssssssssssssssssssysssssssssssssssssz-pxq1419aa69989dba9gc") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.Aßbcßßßßdςσßßßßßßßßeßßßßßßßßßßxßßßßßßßßßßyßßßßßßßß̂ßz") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.xn--abcdexyz-qyacaaabaaaaaaabaaaaaaaaabaaaaaaaaabaaaaaaaa010ze2isb1140zba8cc") C1; C2; A4_2 (ignored)] - expected: FAIL - - [ToASCII("xn--bß") C1; C2] - expected: FAIL - - [ToASCII("XN--BSS") C1; C2] - expected: FAIL - - [ToASCII("xn--bss") C1; C2] - expected: FAIL - - [ToASCII("Xn--Bss") C1; C2] - expected: FAIL - - [ToASCII("xn--xn--bss-7z6ccid") C1; C2] - expected: FAIL - - [ToASCII("Xn--Bß") C1; C2] - expected: FAIL - - [ToASCII("xn--xn--b-pqa5796ccahd") C1; C2] - expected: FAIL - - [ToASCII("ஹ") C2] - expected: FAIL - - [ToASCII("xn--dmc225h") C2] - expected: FAIL - - [ToASCII("") C2] - expected: FAIL - - [ToASCII("xn--1ug") C2] - expected: FAIL - - [ToASCII("ஹ") C1] - expected: FAIL - - [ToASCII("xn--dmc025h") C1] - expected: FAIL - - [ToASCII("") C1] - expected: FAIL - - [ToASCII("xn--0ug") C1] - expected: FAIL - - [ToASCII("ۯۯ") C1] - expected: FAIL - - [ToASCII("xn--cmba004q") C1] - expected: FAIL - - [ToASCII("ß۫。") C2] - expected: FAIL - - [ToASCII("SS۫。") C2] - expected: FAIL - - [ToASCII("ss۫。") C2] - expected: FAIL - - [ToASCII("Ss۫。") C2] - expected: FAIL - - [ToASCII("xn--ss-59d.xn--1ug") C2] - expected: FAIL - - [ToASCII("xn--zca012a.xn--1ug") C2] - expected: FAIL - - [ToASCII("긃.榶-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("긃.榶-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn--0ug3307c.xn----d87b") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----1fa1788k.xn--0ug") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("å둄-.") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ꡦᡑ1.。𐋣-") C2; V3 (ignored); A4_2 (ignored)] - expected: FAIL - - [ToASCII("xn--1-o7j663bdl7m..xn----381i") C2; V3 (ignored); A4_2 (ignored)] - expected: FAIL - - [ToASCII("1.䰹-。웈") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("1.䰹-。웈") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("1.xn----tgnz80r.xn--kp5b") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("-3.ヌᢕ") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("-3.xn--fbf739aq5o") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("ς-。𝟭-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ς-。1-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Σ-。1-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("σ-。1-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----zmb.xn--1--i1t") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----xmb.xn--1--i1t") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("Σ-。𝟭-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("σ-。𝟭-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡯ⚉姶🄉.۷🎪") C2; P1; V6] - expected: FAIL - - [ToASCII("𝟵隁⯮.᠍") C1] - expected: FAIL - - [ToASCII("9隁⯮.᠍") C1] - expected: FAIL - - [ToASCII("xn--9-mfs8024b.xn--0ug") C1] - expected: FAIL - - [ToASCII("ß꫶ᢥ.⊶ⴡⴖ") C1] - expected: FAIL - - [ToASCII("ss꫶ᢥ.⊶ⴡⴖ") C1] - expected: FAIL - - [ToASCII("xn--ss-4ep585bkm5p.xn--ifh802b6a") C1] - expected: FAIL - - [ToASCII("xn--zca682johfi89m.xn--ifh802b6a") C1] - expected: FAIL - - [ToASCII("ß꫶ᢥ.⊶ⴡⴖ") C1] - expected: FAIL - - [ToASCII("ss꫶ᢥ.⊶ⴡⴖ") C1] - expected: FAIL - - [ToASCII("-。") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("-。") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("-.xn--1ug") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("ς-.ⴣ𦟙") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("σ-.ⴣ𦟙") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----zmb048s.xn--rlj2573p") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----xmb348s.xn--rlj2573p") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("鱊。") C1] - expected: FAIL - - [ToASCII("xn--rt6a.xn--0ug") C1] - expected: FAIL - - [ToASCII("ⴚ。ς") C1] - expected: FAIL - - [ToASCII("ⴚ。σ") C1] - expected: FAIL - - [ToASCII("xn--0ug262c.xn--4xa") C1] - expected: FAIL - - [ToASCII("xn--0ug262c.xn--3xa") C1] - expected: FAIL - - [ToASCII("ⴚ。ς") C1] - expected: FAIL - - [ToASCII("ⴚ。σ") C1] - expected: FAIL - - [ToASCII("⾕。꥓̐ꡎ") C1; C2] - expected: FAIL - - [ToASCII("⾕。꥓̐ꡎ") C1; C2] - expected: FAIL - - [ToASCII("谷。꥓̐ꡎ") C1; C2] - expected: FAIL - - [ToASCII("xn--1ug0273b.xn--0sa359l6n7g13a") C1; C2] - expected: FAIL - - [ToASCII("。") C1; C2] - expected: FAIL - - [ToASCII("xn--1ug.xn--0ug") C1; C2] - expected: FAIL - - [ToASCII("。。") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII("xn--0ug..") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-𝟹.ß--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-3.ß--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-3.SS--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-3.ss--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-3.Ss--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn---3-p9o.xn--ss---276a") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn---3-p9o.xn-----fia9303a") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-𝟹.SS--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-𝟹.ss--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ᡲ-𝟹.Ss--") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("𝟙。𝟸⁷") C2] - expected: FAIL - - [ToASCII("1。27") C2] - expected: FAIL - - [ToASCII("1.xn--27-l1tb") C2] - expected: FAIL - - [ToASCII(".ßⴉ-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII(".ssⴉ-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII(".Ssⴉ-") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn--0ug.xn--ss--bi1b") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("xn--0ug.xn----pfa2305a") C1; V3 (ignored)] - expected: FAIL - - [ToASCII("ⴏ󠅋-.ⴉ") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----3vs.xn--1ug532c") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("ⴏ󠅋-.ⴉ") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("。ⴖͦ.") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII(".xn--hva754s.xn--0ug") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII("攌꯭。ᢖ-ⴘ") C2] - expected: FAIL - - [ToASCII("xn--1ug592ykp6b.xn----mck373i") C2] - expected: FAIL - - [ToASCII("ꖨ.16.3툒۳") C1] - expected: FAIL - - [ToASCII("ꖨ.16.3툒۳") C1] - expected: FAIL - - [ToASCII("xn--0ug2473c.16.xn--3-nyc0117m") C1] - expected: FAIL - - [ToASCII("𝟏𝨙⸖.") C2] - expected: FAIL - - [ToASCII("1𝨙⸖.") C2] - expected: FAIL - - [ToASCII("xn--1-5bt6845n.xn--1ug") C2] - expected: FAIL - - [ToASCII("-.ⴞ𐋷") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("xn----ugn.xn--mlj8559d") C2; V3 (ignored)] - expected: FAIL - - [ToASCII("嬃𝍌.ୄ") C2] - expected: FAIL - - [ToASCII("嬃𝍌.ୄ") C2] - expected: FAIL - - [ToASCII("xn--b6s0078f.xn--0ic557h") C2] - expected: FAIL - - [ToASCII(".F") C2] - expected: FAIL - - [ToASCII(".f") C2] - expected: FAIL - - [ToASCII("xn--1ug.f") C2] - expected: FAIL - - [ToASCII("㨲。ß") C2] - expected: FAIL - - [ToASCII("㨲。ß") C2] - expected: FAIL - - [ToASCII("㨲。SS") C2] - expected: FAIL - - [ToASCII("㨲。ss") C2] - expected: FAIL - - [ToASCII("㨲。Ss") C2] - expected: FAIL - - [ToASCII("xn--1ug914h.ss") C2] - expected: FAIL - - [ToASCII("xn--1ug914h.xn--zca") C2] - expected: FAIL - - [ToASCII("㨲。SS") C2] - expected: FAIL - - [ToASCII("㨲。ss") C2] - expected: FAIL - - [ToASCII("㨲。Ss") C2] - expected: FAIL - - [ToASCII("璼𝨭。󠇟") C1] - expected: FAIL - - [ToASCII("璼𝨭。󠇟") C1] - expected: FAIL - - [ToASCII("xn--gky8837e.xn--0ug") C1] - expected: FAIL - - [ToASCII(".") C1] - expected: FAIL - - [ToASCII("xn--0ug.xn--0ug") C1] - expected: FAIL - - [ToASCII("𝟠4󠇗𝈻.𐋵⛧") C2] - expected: FAIL - - [ToASCII("84󠇗𝈻.𐋵⛧") C2] - expected: FAIL - - [ToASCII("xn--84-s850a.xn--1uga573cfq1w") C2] - expected: FAIL - - [ToASCII("󠆪。ß𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。ß𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。SS𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。ss𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。Ss𑓃") C1; C2] - expected: FAIL - - [ToASCII("xn--0ugb.xn--ss-bh7o") C1; C2] - expected: FAIL - - [ToASCII("xn--0ugb.xn--zca0732l") C1; C2] - expected: FAIL - - [ToASCII("󠆪。SS𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。ss𑓃") C1; C2] - expected: FAIL - - [ToASCII("󠆪。Ss𑓃") C1; C2] - expected: FAIL - - [ToASCII("。ヶ䒩.ꡪ") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII(".xn--0ug287dj0o.xn--gd9a") C1; A4_2 (ignored)] - expected: FAIL - - [ToASCII("梉。") C1] - expected: FAIL - - [ToASCII("xn--7zv.xn--0ug") C1] - expected: FAIL - - [ToASCII("𐋷。") C2] - expected: FAIL - - [ToASCII("xn--r97c.xn--1ug") C2] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/url/a-element-xhtml.xhtml.ini b/tests/wpt/meta-legacy-layout/url/a-element-xhtml.xhtml.ini index de5968b9ebf..dbc252fa884 100644 --- a/tests/wpt/meta-legacy-layout/url/a-element-xhtml.xhtml.ini +++ b/tests/wpt/meta-legacy-layout/url/a-element-xhtml.xhtml.ini @@ -269,6 +269,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> against <about:blank>] + expected: FAIL + [a-element-xhtml.xhtml?exclude=(file|javascript|mailto)] [Parsing: <///test> against <http://example.org/>] diff --git a/tests/wpt/meta-legacy-layout/url/a-element.html.ini b/tests/wpt/meta-legacy-layout/url/a-element.html.ini index f7d776d2da7..1d7fbf7763e 100644 --- a/tests/wpt/meta-legacy-layout/url/a-element.html.ini +++ b/tests/wpt/meta-legacy-layout/url/a-element.html.ini @@ -273,6 +273,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> against <about:blank>] + expected: FAIL + [a-element.html?exclude=(file|javascript|mailto)] [Parsing: <///test> against <http://example.org/>] diff --git a/tests/wpt/meta-legacy-layout/url/toascii.window.js.ini b/tests/wpt/meta-legacy-layout/url/toascii.window.js.ini index 56b224e82de..875f6931f54 100644 --- a/tests/wpt/meta-legacy-layout/url/toascii.window.js.ini +++ b/tests/wpt/meta-legacy-layout/url/toascii.window.js.ini @@ -69,24 +69,6 @@ [ab--c.β (using <area>.hostname)] expected: FAIL - [.example (using URL)] - expected: FAIL - - [.example (using URL.host)] - expected: FAIL - - [.example (using URL.hostname)] - expected: FAIL - - [.example (using <a>)] - expected: FAIL - - [.example (using <a>.host)] - expected: FAIL - - [.example (using <a>.hostname)] - expected: FAIL - [.example (using <area>)] expected: FAIL @@ -171,24 +153,6 @@ [01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.β (using <area>.hostname)] expected: FAIL - [xn--1ug.example (using URL)] - expected: FAIL - - [xn--1ug.example (using URL.host)] - expected: FAIL - - [xn--1ug.example (using URL.hostname)] - expected: FAIL - - [xn--1ug.example (using <a>)] - expected: FAIL - - [xn--1ug.example (using <a>.host)] - expected: FAIL - - [xn--1ug.example (using <a>.hostname)] - expected: FAIL - [a%C2%ADb (using <area>.hostname)] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/url/url-constructor.any.js.ini b/tests/wpt/meta-legacy-layout/url/url-constructor.any.js.ini index 379cfa2f247..f97367765c3 100644 --- a/tests/wpt/meta-legacy-layout/url/url-constructor.any.js.ini +++ b/tests/wpt/meta-legacy-layout/url/url-constructor.any.js.ini @@ -456,6 +456,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.html?include=mailto] @@ -658,6 +661,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.html?include=file] [Parsing: </> against <file://h/C:/a/b>] @@ -819,6 +825,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.html?include=javascript] diff --git a/tests/wpt/meta-legacy-layout/url/url-setters-a-area.window.js.ini b/tests/wpt/meta-legacy-layout/url/url-setters-a-area.window.js.ini index a1f221d254d..96439feb297 100644 --- a/tests/wpt/meta-legacy-layout/url/url-setters-a-area.window.js.ini +++ b/tests/wpt/meta-legacy-layout/url/url-setters-a-area.window.js.ini @@ -1108,9 +1108,6 @@ [<area>: Setting <sc://x/>.port = '12'] expected: FAIL - [<a>: Setting <foo://somehost/some/path>.pathname = '' Non-special URLs can have their paths erased] - expected: FAIL - [<area>: Setting <foo://somehost/some/path>.pathname = '' Non-special URLs can have their paths erased] expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/url/url-setters.any.js.ini b/tests/wpt/meta-legacy-layout/url/url-setters.any.js.ini index fc844abd2cd..8d1f09d93c1 100644 --- a/tests/wpt/meta-legacy-layout/url/url-setters.any.js.ini +++ b/tests/wpt/meta-legacy-layout/url/url-setters.any.js.ini @@ -69,9 +69,6 @@ [URL: Setting <http://example.net:8080/path>.hostname = 'example.com:' : delimiter invalidates entire value] expected: FAIL - [URL: Setting <foo://somehost/some/path>.pathname = '' Non-special URLs can have their paths erased] - expected: FAIL - [URL: Setting <foo:///some/path>.pathname = '' Non-special URLs with an empty host can have their paths erased] expected: FAIL @@ -149,9 +146,6 @@ [URL: Setting <http://example.net:8080/path>.hostname = 'example.com:' : delimiter invalidates entire value] expected: FAIL - [URL: Setting <foo://somehost/some/path>.pathname = '' Non-special URLs can have their paths erased] - expected: FAIL - [URL: Setting <foo:///some/path>.pathname = '' Non-special URLs with an empty host can have their paths erased] expected: FAIL diff --git a/tests/wpt/meta/FileAPI/idlharness.any.js.ini b/tests/wpt/meta/FileAPI/idlharness.any.js.ini index cb267114f26..b82efb20e2f 100644 --- a/tests/wpt/meta/FileAPI/idlharness.any.js.ini +++ b/tests/wpt/meta/FileAPI/idlharness.any.js.ini @@ -14,6 +14,15 @@ [FileReader interface: calling readAsBinaryString(Blob) on new FileReader() with too few arguments must throw TypeError] expected: FAIL + [Blob interface: operation bytes()] + expected: FAIL + + [Blob interface: new Blob(["TEST"\]) must inherit property "bytes()" with the proper type] + expected: FAIL + + [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "bytes()" with the proper type] + expected: FAIL + [idlharness.any.worker.html] [Blob interface: operation text()] @@ -30,3 +39,12 @@ [FileReader interface: calling readAsBinaryString(Blob) on new FileReader() with too few arguments must throw TypeError] expected: FAIL + + [Blob interface: operation bytes()] + expected: FAIL + + [Blob interface: new Blob(["TEST"\]) must inherit property "bytes()" with the proper type] + expected: FAIL + + [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "bytes()" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta/FileAPI/idlharness.html.ini b/tests/wpt/meta/FileAPI/idlharness.html.ini index 1209b5650de..6af25e52568 100644 --- a/tests/wpt/meta/FileAPI/idlharness.html.ini +++ b/tests/wpt/meta/FileAPI/idlharness.html.ini @@ -19,3 +19,6 @@ [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "stream()" with the proper type] expected: FAIL + + [Blob interface: operation bytes()] + expected: FAIL diff --git a/tests/wpt/meta/FileAPI/idlharness.worker.js.ini b/tests/wpt/meta/FileAPI/idlharness.worker.js.ini index 4547a92211e..16b61ac30c2 100644 --- a/tests/wpt/meta/FileAPI/idlharness.worker.js.ini +++ b/tests/wpt/meta/FileAPI/idlharness.worker.js.ini @@ -19,3 +19,6 @@ [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "stream()" with the proper type] expected: FAIL + + [Blob interface: operation bytes()] + expected: FAIL diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index 81817bf2aba..f1145817671 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -667,6 +667,13 @@ {} ] ], + "inline-grid-try-options-crash.html": [ + "712f7f1d4817a727362e7b882956bafb59c746bc", + [ + null, + {} + ] + ], "position-try-invalid-anchor-crash.html": [ "dc09bb4b3f8d61652f411fb3cc72e4acd81ef8c8", [ @@ -2561,6 +2568,17 @@ {} ] ], + "initial-letter": { + "crashtests": { + "initial-letter-dynamic-crash.html": [ + "f20f6733114c95f48f010462ec85ccae3a236e7b", + [ + null, + {} + ] + ] + } + }, "inline-002-crash.html": [ "a10eee9907f3ca27b3265452aeb19e2b9710b55d", [ @@ -3725,6 +3743,22 @@ {} ] ], + "line-clamp": { + "webkit-line-clamp-041-crash.html": [ + "3f4bf3c8ef578fa22469e81e504879aa5d713bf8", + [ + null, + {} + ] + ], + "webkit-line-clamp-042-crash.html": [ + "1a5fe2e246ff5abb8b4096223c8f0fd47a3968b2", + [ + null, + {} + ] + ] + }, "multicol-in-orthogonal-writing-mode-crash.html": [ "5a7378de2ea890fc2eefd91135c6cb299a7464ba", [ @@ -3766,20 +3800,6 @@ null, {} ] - ], - "webkit-line-clamp-041-crash.html": [ - "3f4bf3c8ef578fa22469e81e504879aa5d713bf8", - [ - null, - {} - ] - ], - "webkit-line-clamp-042-crash.html": [ - "1a5fe2e246ff5abb8b4096223c8f0fd47a3968b2", - [ - null, - {} - ] ] }, "css-page": { @@ -3976,6 +3996,13 @@ {} ] ], + "first-letter-bidi-pre-crash.html": [ + "fbff1c8f0bc5fd295be094d5a2db36315ef808f7", + [ + null, + {} + ] + ], "first-letter-crash.html": [ "683d2a77662149be28c581c5de6651e776653d65", [ @@ -5937,6 +5964,13 @@ {} ] ], + "insertparagraph-in-editable-dl-outside-body.html": [ + "4c04d9590754cc81ba6ca66bac9312c3a99d5615", + [ + null, + {} + ] + ], "insertparagraph-in-listitem-in-svg-followed-by-collapsible-spaces.html": [ "f5f981965ba99aaf1f9ab0965238222094114a77", [ @@ -6907,6 +6941,13 @@ null, {} ] + ], + "textarea-update-default-value-in-shadow-crash.html": [ + "f2b040434f41ea9c6e746544d73b76cd9f1ec622", + [ + null, + {} + ] ] } }, @@ -28352,7 +28393,7 @@ ] ], "types-dom-01-b-manual.svg": [ - "60093ac542094ae8021e034dc52e1f7fd305f008", + "cf6e3d38f15859411a6cf52c65491831ebddab1a", [ null, {} @@ -32768,6 +32809,19 @@ {} ] ], + "page-background-image-print.html": [ + "633cd4ec6c9027902782f271c2161394cf6cb5e6", + [ + null, + [ + [ + "/css/css-page/page-background-image-print-ref.html", + "==" + ] + ], + {} + ] + ], "page-box-000-print.html": [ "aee317ab9752b33c91bc35d3cca5c57dd2212c9f", [ @@ -125917,6 +125971,19 @@ ] ], "background-size": { + "background-size-contain-svg-view.html": [ + "0d69e27e3dc653b7c4dc5f4da6330bd811a7c112", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square-only.html", + "==" + ] + ], + {} + ] + ], "background-size-contain.xht": [ "7309905bbac46d67f16907d799fb3ba7716bb31a", [ @@ -125956,6 +126023,19 @@ {} ] ], + "background-size-cover-svg-view.html": [ + "dc792792476aea43d1f3239a19cdae2ebfd2c484", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square-only.html", + "==" + ] + ], + {} + ] + ], "background-size-cover-svg.html": [ "67f3bcac7d5da84f4e1c120974ca0ceeeaf6ffa0", [ @@ -129824,7 +129904,7 @@ ] ], "border-image-image-type-001.htm": [ - "9486f0b8b6e2d5fcccda1e7d44084db4c6722219", + "2c8d5941a4dbc6166fabd5d16c0f2f5f6128a129", [ null, [ @@ -129833,7 +129913,23 @@ "==" ] ], - {} + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 64 + ], + [ + 0, + 400 + ] + ] + ] + ] + } ] ], "border-image-image-type-002.htm": [ @@ -174470,6 +174566,32 @@ {} ] ], + "remove-wrapped-001.html": [ + "fd38ccce27acd0cdf89a96473dbc65b963870925", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "remove-wrapped-002.html": [ + "a8048b84a3bafe0e2f65b6990e2e4e68922b4222", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "scrollbars-auto.html": [ "ed1ffb8d93aec7c95eb3c9bbdbb6bd31ae402593", [ @@ -178149,6 +178271,32 @@ {} ] ], + "font-variant-emoji-003.html": [ + "e197afa7c40f6537a7ab3c57c7143b52126d139f", + [ + null, + [ + [ + "/css/css-fonts/font-variant-emoji-003-ref.html", + "==" + ] + ], + {} + ] + ], + "font-variant-emoji-004.html": [ + "d76828dff01d5fab2b374656caab1003d2f84840", + [ + null, + [ + [ + "/css/css-fonts/font-variant-emoji-004-ref.html", + "==" + ] + ], + {} + ] + ], "font-variant-emoji-1.html": [ "53cf539edd8b61a7f08f8b95ae83f4f7a5179308", [ @@ -183308,7 +183456,7 @@ ] ], "empty-grid-within-flexbox.html": [ - "1941800cb187428ebecf56cbbd2ca9d1bbdfd563", + "ee67486474a9e06e6418aa71e6a3d283ae9c5dbc", [ null, [ @@ -196826,6 +196974,32 @@ {} ] ], + "text-box-trim-height-001.html": [ + "d732592e51ad1d55b77fb295b9ba66dab8287ada", + [ + null, + [ + [ + "/css/css-inline/text-box-trim/text-box-trim-height-001-ref.html", + "==" + ] + ], + {} + ] + ], + "text-box-trim-height-002.html": [ + "1249627f8385832598b1e4e5549eb1445b098b47", + [ + null, + [ + [ + "/css/css-inline/text-box-trim/text-box-trim-height-001-ref.html", + "==" + ] + ], + {} + ] + ], "text-box-trim-initial-letter-end-001.html": [ "809666ee0d07e6b4cc4dc6a9509d188392d59ded", [ @@ -199333,7 +199507,7 @@ ] ], "counter-set-001.html": [ - "6ec53e3bfa12306db12ecdded3f6727053222094", + "4e28367798ae65095b21094d2d5da777b57f4294", [ null, [ @@ -199437,7 +199611,7 @@ ] ], "counters-005.html": [ - "12908e64720863293bb71dd0867524921bf34273", + "528c0ede8701d1ad3b68b1dc3532123cc59b2b98", [ null, [ @@ -199449,6 +199623,19 @@ {} ] ], + "counters-006.html": [ + "63a65a7ff83a8adbd193034447ef582a6eb8a453", + [ + null, + [ + [ + "/css/css-lists/counters-006-ref.html", + "==" + ] + ], + {} + ] + ], "counters-scope-001.html": [ "2ea72753bd984fe93db1872d5f90e72da6cd4966", [ @@ -213261,760 +213448,1321 @@ {} ] ], - "line-clamp-001.tentative.html": [ - "c8cfcb1066de1a1d5c98e147c8e47499069046be", - [ - null, + "line-clamp": { + "line-clamp-001.html": [ + "c8cfcb1066de1a1d5c98e147c8e47499069046be", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-002.tentative.html": [ - "5f21b545fb32d8ecb2c72287b9fc9fcf9d41de90", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-002.html": [ + "5f21b545fb32d8ecb2c72287b9fc9fcf9d41de90", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-003.tentative.html": [ - "fa3b7472e5e44fe9eb854eb4dadc34f98af16e92", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-003.html": [ + "fa3b7472e5e44fe9eb854eb4dadc34f98af16e92", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-004.tentative.html": [ - "c766d195b7c27935f099876f8e69a18966623ad9", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-004.html": [ + "c766d195b7c27935f099876f8e69a18966623ad9", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-006-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-005.tentative.html": [ - "143aa65d899dc1e83e5be5927f1f349e8f0a399c", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-006-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-005.tentative.html": [ + "143aa65d899dc1e83e5be5927f1f349e8f0a399c", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-009-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-006.tentative.html": [ - "f06d94161b635a092ed3446cdf9e6b3ebf830777", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-009-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-006.tentative.html": [ + "f06d94161b635a092ed3446cdf9e6b3ebf830777", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-010-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-007.tentative.html": [ - "c71068641b80530031dc4474422225033959d146", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-010-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-007.tentative.html": [ + "c71068641b80530031dc4474422225033959d146", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-011-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-008.tentative.html": [ - "0d91b3612d0cd38a08f56f01d0d38a7e3cc62042", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-011-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-008.tentative.html": [ + "0d91b3612d0cd38a08f56f01d0d38a7e3cc62042", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-009.tentative.html": [ - "4dfd3d6194a6f3715f4f684b8072b5be55478911", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-009.tentative.html": [ + "4dfd3d6194a6f3715f4f684b8072b5be55478911", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-006-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-010.tentative.html": [ - "1386b147ce639f511fa1fc58e0c0cd75ab99efc2", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-006-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-010.html": [ + "1386b147ce639f511fa1fc58e0c0cd75ab99efc2", [ + null, [ - "/css/css-overflow/reference/line-clamp-010-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-011.tentative.html": [ - "953f0c4faa9e57975cfabfd10d20ae4e1f9c45a3", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-010-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-011.tentative.html": [ + "953f0c4faa9e57975cfabfd10d20ae4e1f9c45a3", [ + null, [ - "/css/css-overflow/reference/line-clamp-011-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-012.tentative.html": [ - "be39074037745b5bda0092ff29d45c68f93b4442", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-011-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-012.tentative.html": [ + "be39074037745b5bda0092ff29d45c68f93b4442", [ + null, [ - "/css/css-overflow/reference/line-clamp-012-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-013.tentative.html": [ - "1bda501f028e9cca1f0f7f5930d4459ac26a5607", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-012-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-013.tentative.html": [ + "1bda501f028e9cca1f0f7f5930d4459ac26a5607", [ + null, [ - "/css/css-overflow/reference/line-clamp-013-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-014.tentative.html": [ - "9ca7c8937248f482aec2976490c2c86b945f0dfd", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-013-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-014.html": [ + "9ca7c8937248f482aec2976490c2c86b945f0dfd", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-015.tentative.html": [ - "820300732235ceed5333217a3cc856c224e8e456", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-015.html": [ + "820300732235ceed5333217a3cc856c224e8e456", [ + null, [ - "/css/css-overflow/reference/line-clamp-015-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-016.tentative.html": [ - "09714c499de663e621925c74bc63e167dc9f7b85", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-015-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-016.tentative.html": [ + "09714c499de663e621925c74bc63e167dc9f7b85", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-017.tentative.html": [ - "11d6ceeb556c7469b8de0cd323ddb2594e8b8b34", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-017.tentative.html": [ + "11d6ceeb556c7469b8de0cd323ddb2594e8b8b34", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-018.tentative.html": [ - "af75f7dfb0d291f6158fea3081ca5d1042d06989", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-018.tentative.html": [ + "af75f7dfb0d291f6158fea3081ca5d1042d06989", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-019.tentative.html": [ - "b39376d39502ff643d41543c26db8f43abcc35bd", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-019.tentative.html": [ + "b39376d39502ff643d41543c26db8f43abcc35bd", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-020.tentative.html": [ - "9d8a2b4d06ce8d1ca1c8a90ec44abbf2466fb7a6", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-020.html": [ + "9d8a2b4d06ce8d1ca1c8a90ec44abbf2466fb7a6", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-021.tentative.html": [ - "611cd6f8901af66ecdc301305f2db0f30bf96023", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-021.tentative.html": [ + "611cd6f8901af66ecdc301305f2db0f30bf96023", [ + null, [ - "/css/css-overflow/reference/line-clamp-021-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-001.tentative.html": [ - "02d8479736d4d6fd7e26df53611624e7a75d0989", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-021-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-001.tentative.html": [ + "02d8479736d4d6fd7e26df53611624e7a75d0989", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-002.tentative.html": [ - "ff9e802f0f9e376635230707972611ab5a0ec7d6", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-002.tentative.html": [ + "ff9e802f0f9e376635230707972611ab5a0ec7d6", [ + null, [ - "/css/css-overflow/reference/line-clamp-auto-002-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-003.tentative.html": [ - "a74704dd3b0765434c27935c944fac010415b13c", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-auto-002-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-003.tentative.html": [ + "a74704dd3b0765434c27935c944fac010415b13c", [ + null, [ - "/css/css-overflow/reference/line-clamp-auto-002-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-004.tentative.html": [ - "2dbf9d54084034f2a1b83eccf370e281af24dc8c", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-auto-002-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-004.tentative.html": [ + "2dbf9d54084034f2a1b83eccf370e281af24dc8c", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-005.tentative.html": [ - "1c9148e2647f310522103d1b5c6c2765a6c6644c", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-005.tentative.html": [ + "1c9148e2647f310522103d1b5c6c2765a6c6644c", [ + null, [ - "/css/css-overflow/reference/line-clamp-auto-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-006.tentative.html": [ - "ab6915f52163f9cbd1c4c2f2fa4e9d8bf90a3a88", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-auto-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-006.tentative.html": [ + "ab6915f52163f9cbd1c4c2f2fa4e9d8bf90a3a88", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-007.tentative.html": [ - "f7d56bfa6557413e33bbb2422c5234437ac302b4", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-007.tentative.html": [ + "f7d56bfa6557413e33bbb2422c5234437ac302b4", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-008.tentative.html": [ - "9e7f38ab7ca09debf7628505d8d63ce473dc355e", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-008.tentative.html": [ + "9e7f38ab7ca09debf7628505d8d63ce473dc355e", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-009.tentative.html": [ - "44d111056c6b021c796ea694d744816125314def", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-009.tentative.html": [ + "44d111056c6b021c796ea694d744816125314def", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-036-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-010.tentative.html": [ - "cb706bba08e2c9dd6ff05b926be23eb22c9c25b7", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-036-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-010.tentative.html": [ + "cb706bba08e2c9dd6ff05b926be23eb22c9c25b7", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-037-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-011.tentative.html": [ - "00076a5336e89004b6a2143e9bd6c72ec9fe95e0", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-037-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-011.tentative.html": [ + "00076a5336e89004b6a2143e9bd6c72ec9fe95e0", [ + null, [ - "/css/css-overflow/reference/line-clamp-auto-011-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-012.tentative.html": [ - "56957b15c9223466ddd6f06a45bb854cab18fda6", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-auto-011-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-012.tentative.html": [ + "56957b15c9223466ddd6f06a45bb854cab18fda6", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-013.tentative.html": [ - "dd864fbdcb3fbd3cfeb11a385df61a99e739aba9", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-013.tentative.html": [ + "dd864fbdcb3fbd3cfeb11a385df61a99e739aba9", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-014.tentative.html": [ - "6738c708706e778dd4ee307b8532ca32e8b7b881", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-014.tentative.html": [ + "6738c708706e778dd4ee307b8532ca32e8b7b881", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-015.tentative.html": [ - "cdb1ed18c0d7a913fc3b9e446dbff15add266300", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-015.tentative.html": [ + "cdb1ed18c0d7a913fc3b9e446dbff15add266300", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-auto-016.tentative.html": [ - "372213983b7a753037616485bfc2aae7524abfd5", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-auto-016.tentative.html": [ + "372213983b7a753037616485bfc2aae7524abfd5", [ + null, [ - "/css/css-overflow/reference/line-clamp-auto-016-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-001.tentative.html": [ - "79667f23fbdc3d941484c343b2cf0a04ec34363f", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-auto-016-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-001.tentative.html": [ + "79667f23fbdc3d941484c343b2cf0a04ec34363f", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-002.tentative.html": [ - "cecb9d52bc63ec161dd96dfadcef166edd9334ed", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-002.tentative.html": [ + "cecb9d52bc63ec161dd96dfadcef166edd9334ed", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-003.tentative.html": [ - "e4bd1de222e2b75455ae50d4c8bfa5c62b0c47c2", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-003.tentative.html": [ + "e4bd1de222e2b75455ae50d4c8bfa5c62b0c47c2", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-004.tentative.html": [ - "483e6d1da6dc739599e25f3172b0ad248ac23c1b", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-004.tentative.html": [ + "483e6d1da6dc739599e25f3172b0ad248ac23c1b", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-005.tentative.html": [ - "3dc77831a06ffd28df3a5a4dca6045cba30fe1dd", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-005.tentative.html": [ + "3dc77831a06ffd28df3a5a4dca6045cba30fe1dd", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-006.tentative.html": [ - "f18fed6c2daea0ab6e1ab715c1257407067fa97b", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-006.tentative.html": [ + "f18fed6c2daea0ab6e1ab715c1257407067fa97b", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-006-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-007.tentative.html": [ - "f0a1f58c8d304aead5490db007f763d437e9c5ad", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-006-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-007.tentative.html": [ + "f0a1f58c8d304aead5490db007f763d437e9c5ad", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-007-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-008.tentative.html": [ - "9c62e44f3897b5868dde26a6d6c203c7ddea4b8a", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-007-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-008.tentative.html": [ + "9c62e44f3897b5868dde26a6d6c203c7ddea4b8a", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-008-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-009.tentative.html": [ - "dce04d720cfc83d82e2228204b7f6d41e244aabf", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-008-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-009.tentative.html": [ + "dce04d720cfc83d82e2228204b7f6d41e244aabf", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-010.tentative.html": [ - "325278b3a0b44f480ea020c575b9427b33f2ec4e", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-010.tentative.html": [ + "325278b3a0b44f480ea020c575b9427b33f2ec4e", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-010-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-abspos-011.tentative.html": [ - "ab5102a7cf2ea75cdb4729092d7618882e9c6ddf", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-010-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-abspos-011.tentative.html": [ + "ab5102a7cf2ea75cdb4729092d7618882e9c6ddf", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-001.tentative.html": [ - "98bbdcb9040224c9c9df1a535ffdeb426040a3c7", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-011-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-001.tentative.html": [ + "98bbdcb9040224c9c9df1a535ffdeb426040a3c7", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-002.tentative.html": [ - "15379a3de3e5518505c24ed9722c0ac4541331a2", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-002.tentative.html": [ + "15379a3de3e5518505c24ed9722c0ac4541331a2", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-001-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-003.tentative.html": [ - "c203758235130cd8e4a0c4838b6b99f8c284cd20", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-001-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-003.tentative.html": [ + "c203758235130cd8e4a0c4838b6b99f8c284cd20", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-004.tentative.html": [ - "6213130174768418c078ffc792c30b2774c5d9c6", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-004.tentative.html": [ + "6213130174768418c078ffc792c30b2774c5d9c6", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-005.tentative.html": [ - "9689a83fa7f9e5976c2d5b71f6adc7fd0397e2cc", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-005.tentative.html": [ + "9689a83fa7f9e5976c2d5b71f6adc7fd0397e2cc", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-006.tentative.html": [ - "0a709bff198f3f4c35072559ab67f9892a1bf668", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-006.tentative.html": [ + "0a709bff198f3f4c35072559ab67f9892a1bf668", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-006-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-007.tentative.html": [ - "7ee286fbf43b3d961d8a26dc0063b38523e49ad5", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-006-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-007.tentative.html": [ + "7ee286fbf43b3d961d8a26dc0063b38523e49ad5", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-007-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-008.tentative.html": [ - "c62ba5371a8fe27ab4e463dc7e548135b7561701", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-007-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-008.tentative.html": [ + "c62ba5371a8fe27ab4e463dc7e548135b7561701", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-008-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-009.tentative.html": [ - "f25ac381c09d8766f1fd8efe9ec8b677a55b2988", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-008-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-009.tentative.html": [ + "f25ac381c09d8766f1fd8efe9ec8b677a55b2988", [ + null, [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "line-clamp-with-floats-010.tentative.html": [ - "a00ff60171258aee2aba3ac98847e5756fbc26eb", - [ - null, + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "line-clamp-with-floats-010.tentative.html": [ + "a00ff60171258aee2aba3ac98847e5756fbc26eb", [ + null, [ - "/css/css-overflow/reference/line-clamp-with-floats-010-ref.html", - "==" - ] - ], - {} + [ + "/css/css-overflow/line-clamp/reference/line-clamp-with-floats-010-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-001.html": [ + "bba3d1c49f2dab5055fbceb7313659f463454166", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-002.html": [ + "a04b95994204c1ae2e5b6173719fb9ea41f126c2", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-003.html": [ + "5ebe64675f8c76eb4b59a6a19cd8d79ddf9a75f9", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-004.html": [ + "d66d0bb62f12339c0f04c773af2e8daa82739548", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-005.html": [ + "73b4b8cfa8b5ba2a111d38f0b334f51b59da81e4", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-006.html": [ + "0d2c1f9c6458bf4d4c1f43e9337d0bb306957cdf", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-006-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-007.html": [ + "95c2db51653c2ef59cf9b1962354d0db1bb48015", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-007-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-008.html": [ + "471333e666f17ef780054f1cd456106bc689d9f3", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-008-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-009.html": [ + "240f6b113b08f6725256f2b3c4d937cc1ac66970", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-009-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-010.html": [ + "9ff23a2fe399bcfc2ee2e39a76f2cb504cecfaa7", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-010-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-011.html": [ + "3a0016e9dfdac662089a7898fa30a6edac4c999c", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-011-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-012.html": [ + "d46a7944fafb31945a43333c8707078e99e6e999", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-012-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-013.html": [ + "6db53906acd2f1354d6de0525fb1f5fa906fbbc8", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-013-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-014.html": [ + "948b247533065dbedc6f6c18e6fba9a2e2715b27", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-014-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-015.html": [ + "c1817e04e6a8ecc227ab93299d004ac0d93487e3", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-015-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-016.html": [ + "5b2d4593aefed4b36653c09394c1fe5e3565aaf3", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-016-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-017.html": [ + "ece4d3123b1b8dda7da814648f2c2e0e862f47bb", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-017-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-018.html": [ + "d381c645467ffa2a95c74191a748a587b8e1c51b", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-018-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-019.html": [ + "ff6f7e3cf788dbda5a6cb18c4e6f4b4724b578b8", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-019-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-020.html": [ + "9d875d52dee852ddf360c1edf1ccc2b7c3e638d1", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-020-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-021.html": [ + "44a196c9ba1b120f99ae4274c1d70bf9544fcefd", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-021-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-022.html": [ + "2959a734411283572b854469f84e547a718ed2a5", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-022-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-023.html": [ + "7f03513baa8eddcebf2a5a1e44fe7062bb5f78cc", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-023-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-024.html": [ + "6ebf5a8958b9592f3fc3850185cd3ee59867beaa", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-024-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-025.html": [ + "67804dedf60db0c75f6da93af3be38398c4cd161", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-025-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-026.html": [ + "741384f292770c3a87d0ade0b3745d29c779d988", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-026-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-027.html": [ + "93bfb0f4b46a0f3c1d2ed9d6dede2892475884fb", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-027-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-029.html": [ + "fc395d313ea5cd8015f56c9fbfec319a24c69622", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-029-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-030.html": [ + "5e14a78a583ed0fc3d50ede16f25df68d7f3d05b", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-030-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-031.html": [ + "4f8c55301ab26a4ba6c5cedbc50abd2f554bfe89", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-031-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-032.html": [ + "4d0731379d042b877926d00236e4e3a7d4d3abea", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-032-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-033.html": [ + "261cf5b6b2478698cd50c25f6d6a437e6853e5b1", + [ + null, + [ + [ + "/css/reference/blank.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-034.html": [ + "e0b31d544c20a0f57486c771b6b4edef9b69625b", + [ + null, + [ + [ + "/css/reference/blank.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-035.html": [ + "5a550e022ecbb87580c3beb77337fccac825adc7", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-035-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-036.html": [ + "b8d7b194f05dd8dc1189900b399188e0d17391d1", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-036-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-037.html": [ + "86015228f88e4c622c4bd1c20cb35a20619075c7", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-037-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-038.html": [ + "b50ae2764c317c809af07649295a2d42ec211517", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-038-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-039.html": [ + "fe9436da53d707f8641e606d5756d3ad2e771b7a", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-039-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-040.html": [ + "fa885c8088d5f2d221548ca062fdc0ebb0c83e4b", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-040-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-043.html": [ + "1f7a0a94c03ed0661c67c4ef46c7ce31d133ddde", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-block-in-inline-001.html": [ + "75d1de3bf5bcf5d00d6980de4a70845e9f7ae8e4", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-block-in-inline-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-dynamic-001.html": [ + "fc4f2f9e4db6b55d124dd774e0563e2625ca3288", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-dynamic-001-ref.html", + "==" + ] + ], + {} + ] + ], + "webkit-line-clamp-with-line-height.tentative.html": [ + "a82635f986b60ff4853785dc40b7bbe985af4eb1", + [ + null, + [ + [ + "/css/css-overflow/line-clamp/reference/webkit-line-clamp-with-line-height-ref.html", + "==" + ] + ], + {} + ] ] - ], + }, "margin-block-end-scroll-area-001.html": [ "3b8f4ca3f355718d05fe6eeda200bddccb388f57", [ @@ -215329,565 +216077,6 @@ ], {} ] - ], - "webkit-line-clamp-001.html": [ - "bba3d1c49f2dab5055fbceb7313659f463454166", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-002.html": [ - "a04b95994204c1ae2e5b6173719fb9ea41f126c2", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-003.html": [ - "5ebe64675f8c76eb4b59a6a19cd8d79ddf9a75f9", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-004.html": [ - "d66d0bb62f12339c0f04c773af2e8daa82739548", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-005.html": [ - "73b4b8cfa8b5ba2a111d38f0b334f51b59da81e4", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-005-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-006.html": [ - "0d2c1f9c6458bf4d4c1f43e9337d0bb306957cdf", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-006-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-007.html": [ - "95c2db51653c2ef59cf9b1962354d0db1bb48015", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-007-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-008.html": [ - "471333e666f17ef780054f1cd456106bc689d9f3", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-008-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-009.html": [ - "240f6b113b08f6725256f2b3c4d937cc1ac66970", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-009-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-010.html": [ - "9ff23a2fe399bcfc2ee2e39a76f2cb504cecfaa7", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-010-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-011.html": [ - "3a0016e9dfdac662089a7898fa30a6edac4c999c", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-011-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-012.html": [ - "d46a7944fafb31945a43333c8707078e99e6e999", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-012-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-013.html": [ - "6db53906acd2f1354d6de0525fb1f5fa906fbbc8", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-013-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-014.html": [ - "948b247533065dbedc6f6c18e6fba9a2e2715b27", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-014-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-015.html": [ - "c1817e04e6a8ecc227ab93299d004ac0d93487e3", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-015-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-016.html": [ - "5b2d4593aefed4b36653c09394c1fe5e3565aaf3", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-016-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-017.html": [ - "ece4d3123b1b8dda7da814648f2c2e0e862f47bb", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-017-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-018.html": [ - "d381c645467ffa2a95c74191a748a587b8e1c51b", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-018-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-019.html": [ - "ff6f7e3cf788dbda5a6cb18c4e6f4b4724b578b8", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-019-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-020.html": [ - "9d875d52dee852ddf360c1edf1ccc2b7c3e638d1", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-020-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-021.html": [ - "44a196c9ba1b120f99ae4274c1d70bf9544fcefd", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-021-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-022.html": [ - "2959a734411283572b854469f84e547a718ed2a5", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-022-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-023.html": [ - "7f03513baa8eddcebf2a5a1e44fe7062bb5f78cc", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-023-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-024.html": [ - "6ebf5a8958b9592f3fc3850185cd3ee59867beaa", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-024-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-025.html": [ - "67804dedf60db0c75f6da93af3be38398c4cd161", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-025-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-026.html": [ - "741384f292770c3a87d0ade0b3745d29c779d988", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-026-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-027.html": [ - "93bfb0f4b46a0f3c1d2ed9d6dede2892475884fb", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-027-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-029.html": [ - "fc395d313ea5cd8015f56c9fbfec319a24c69622", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-029-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-030.html": [ - "5e14a78a583ed0fc3d50ede16f25df68d7f3d05b", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-030-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-031.html": [ - "4f8c55301ab26a4ba6c5cedbc50abd2f554bfe89", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-031-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-032.html": [ - "4d0731379d042b877926d00236e4e3a7d4d3abea", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-032-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-033.html": [ - "261cf5b6b2478698cd50c25f6d6a437e6853e5b1", - [ - null, - [ - [ - "/css/reference/blank.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-034.html": [ - "e0b31d544c20a0f57486c771b6b4edef9b69625b", - [ - null, - [ - [ - "/css/reference/blank.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-035.html": [ - "5a550e022ecbb87580c3beb77337fccac825adc7", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-035-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-036.html": [ - "b8d7b194f05dd8dc1189900b399188e0d17391d1", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-036-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-037.html": [ - "86015228f88e4c622c4bd1c20cb35a20619075c7", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-037-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-038.html": [ - "b50ae2764c317c809af07649295a2d42ec211517", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-038-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-039.html": [ - "fe9436da53d707f8641e606d5756d3ad2e771b7a", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-039-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-040.html": [ - "fa885c8088d5f2d221548ca062fdc0ebb0c83e4b", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-040-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-043.html": [ - "54f0ac538daaa351de0e7d870e8bf35f3a6711f8", - [ - null, - [ - [ - "/css/reference/ref-filled-green-100px-square.xht", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-block-in-inline-001.html": [ - "75d1de3bf5bcf5d00d6980de4a70845e9f7ae8e4", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-block-in-inline-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-dynamic-001.html": [ - "fc4f2f9e4db6b55d124dd774e0563e2625ca3288", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-dynamic-001-ref.html", - "==" - ] - ], - {} - ] - ], - "webkit-line-clamp-with-line-height.tentative.html": [ - "a82635f986b60ff4853785dc40b7bbe985af4eb1", - [ - null, - [ - [ - "/css/css-overflow/reference/webkit-line-clamp-with-line-height-ref.html", - "==" - ] - ], - {} - ] ] }, "css-paint-api": { @@ -220883,6 +221072,19 @@ {} ] ], + "first-letter-width-2.html": [ + "e7d01afe47291b507179ca4f984e710d06ee526e", + [ + null, + [ + [ + "/css/css-pseudo/first-letter-width-2-ref.html", + "==" + ] + ], + {} + ] + ], "first-letter-width.html": [ "fedf917c62a1c34c054b7484594ced16960c4c17", [ @@ -223596,6 +223798,19 @@ {} ] ], + "interlinear-block-margin-box.html": [ + "cf9bb43826e72c321ae25e86a709940da1bea9d6", + [ + null, + [ + [ + "/css/css-ruby/interlinear-block-margin-box-ref.html", + "!=" + ] + ], + {} + ] + ], "intra-base-white-space-001.html": [ "86468376c3211db6dbe6e8fa53b6186b4214af34", [ @@ -223622,6 +223837,19 @@ {} ] ], + "pseudo-first-letter.html": [ + "dcb9e8583b4a8208a912e2588fcfeb394d3832d2", + [ + null, + [ + [ + "/css/css-ruby/pseudo-first-letter-ref.html", + "==" + ] + ], + {} + ] + ], "pseudo-first-line.html": [ "6be0a8b20a8ced882403ff788ed78d641edde5ce", [ @@ -271218,7 +271446,7 @@ ] ], "box-sizing-007.html": [ - "c51f4318db4ed1e37213078db89db09e110f42e2", + "8e3c5a632555785b59999bc631262dcaf37558b3", [ null, [ @@ -271231,7 +271459,7 @@ ] ], "box-sizing-008.html": [ - "3e79e29bcdcf81a02a70edd602b29056ee5e0397", + "f44096038999a0d49a6770f9c61fab5d851d6b92", [ null, [ @@ -271244,7 +271472,7 @@ ] ], "box-sizing-009.html": [ - "69d526d63d3557adbe3c78d1e83a9863b8c4816b", + "b13b64c0f838929e31ec501df105783a0ecec77b", [ null, [ @@ -284629,6 +284857,19 @@ {} ] ], + "rlh-unit-001.html": [ + "c435fd99709dd6a61a9ec128e52958653518b882", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "vh-calc-support-pct.html": [ "3a422d4ce46fd57636eaf564c356f4152ca3a061", [ @@ -287270,6 +287511,19 @@ } ] ], + "active-view-transition-pseudo-class-match.html": [ + "534c144ecf0c3b7dc14fd915782f8daeef93a18c", + [ + null, + [ + [ + "/css/css-view-transitions/view-transition-types-one-green-square-ref.html", + "==" + ] + ], + {} + ] + ], "animating-new-content-subset.html": [ "d5a342c572041f8ef30efef4cfca703636a9266f", [ @@ -289353,6 +289607,19 @@ } ] ], + "new-content-ancestor-clipped-2.html": [ + "3ce02097476ec97219e2991ab1366f1ea746caa1", + [ + null, + [ + [ + "/css/css-view-transitions/new-content-ancestor-clipped-2-ref.html", + "==" + ] + ], + {} + ] + ], "new-content-ancestor-clipped.html": [ "69a8de5f527a459f2a85b562dee46b8298bbb65e", [ @@ -289549,6 +289816,35 @@ {} ] ], + "new-content-flat-transform-ancestor.html": [ + "112a0af2c45d8df8b6e2a8580dece11e86f816e8", + [ + null, + [ + [ + "/css/css-view-transitions/new-content-flat-transform-ancestor-ref.html", + "==" + ] + ], + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 63 + ], + [ + 0, + 510 + ] + ] + ] + ] + } + ] + ], "new-content-from-root-display-none.html": [ "e59c029ccce74d22c553af008d478e8accf3cef1", [ @@ -289592,6 +289888,19 @@ } ] ], + "new-content-inline-with-offset-from-containing-block-clipped.html": [ + "2b122d0a74620db0fa67c545d65a82624fa8e666", + [ + null, + [ + [ + "/css/css-view-transitions/inline-with-offset-from-containing-block-clipped-ref.html", + "==" + ] + ], + {} + ] + ], "new-content-intrinsic-aspect-ratio.html": [ "60f2b92036c246803bae4b5d207e6ff5aa612d0f", [ @@ -289741,6 +290050,48 @@ {} ] ], + "new-content-preserve-3d-ancestor.html": [ + "61f274d276cae22f5f52b45bb16bd3b0600e8b1e", + [ + null, + [ + [ + "/css/css-view-transitions/new-content-preserve-3d-ancestor-ref.html", + "==" + ] + ], + {} + ] + ], + "new-content-root-scrollbar-with-fixed-background.html": [ + "89f8158b58b96886ad0abcc6bb2f1e3c6663ff37", + [ + null, + [ + [ + "/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html", + "==" + ] + ], + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 2 + ], + [ + 0, + 4500 + ] + ] + ] + ] + } + ] + ], "new-content-scaling.html": [ "376b7fd11f7e2e86e549c300fcb09cc8767751c4", [ @@ -289770,6 +290121,19 @@ } ] ], + "new-content-transform-position-fixed.html": [ + "4b27796c9b4fa224b82514fd0c05bea56a125ea1", + [ + null, + [ + [ + "/css/css-view-transitions/new-content-transform-position-fixed-ref.html", + "==" + ] + ], + {} + ] + ], "new-content-with-object-view-box.html": [ "40139bbaa95b8d8ec612bab52ed527d0b5c00455", [ @@ -289837,6 +290201,19 @@ {} ] ], + "no-painting-while-render-blocked.html": [ + "1feb4759eb87e43e125f0753bd3c0c9c889816f7", + [ + null, + [ + [ + "/css/css-view-transitions/no-painting-while-render-blocked-ref.html", + "==" + ] + ], + {} + ] + ], "no-root-capture.html": [ "87e03266004ad19f135a46be47381be5bc94c4cc", [ @@ -290105,6 +290482,19 @@ } ] ], + "old-content-inline-with-offset-from-containing-block-clipped.html": [ + "7f35d8599b37188d0c0838d90126673ef1c88af4", + [ + null, + [ + [ + "/css/css-view-transitions/inline-with-offset-from-containing-block-clipped-ref.html", + "==" + ] + ], + {} + ] + ], "old-content-intrinsic-aspect-ratio.html": [ "bfca9d321abb9c12c2ed933c32771b94c40bf7ae", [ @@ -290273,6 +290663,35 @@ {} ] ], + "old-content-root-scrollbar-with-fixed-background.html": [ + "7c8b7dd92e47d81938fb13fe0de925047f387331", + [ + null, + [ + [ + "/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html", + "==" + ] + ], + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 2 + ], + [ + 0, + 4500 + ] + ] + ] + ] + } + ] + ], "old-content-with-object-view-box.html": [ "df32005546ee27a3edd23a8f3a63e8cc29bf7f15", [ @@ -290590,42 +291009,26 @@ {} ] ], - "root-captured-as-different-tag.html": [ - "1d4d1610d16bd4e6e682acdc19fcad3d48aefa2f", + "reset-state-after-scrolled-view-transition.html": [ + "5f8c14fd9b7bfc520be5b227c163adf4f518eea3", [ null, [ [ - "/css/css-view-transitions/old-content-captures-root-ref.html", + "/css/css-view-transitions/reset-state-after-scrolled-view-transition-ref.html", "==" ] ], - { - "fuzzy": [ - [ - null, - [ - [ - 0, - 1 - ], - [ - 0, - 500 - ] - ] - ] - ] - } + {} ] ], - "root-scrollbar-with-fixed-background.html": [ - "3c3429412bf309be9181f5f4f2d75019b80a2dbf", + "root-captured-as-different-tag.html": [ + "1d4d1610d16bd4e6e682acdc19fcad3d48aefa2f", [ null, [ [ - "/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html", + "/css/css-view-transitions/old-content-captures-root-ref.html", "==" ] ], @@ -290636,11 +291039,11 @@ [ [ 0, - 2 + 1 ], [ 0, - 4500 + 500 ] ] ] @@ -291253,19 +291656,6 @@ {} ] ], - "view-transition-types-universal-match.html": [ - "534c144ecf0c3b7dc14fd915782f8daeef93a18c", - [ - null, - [ - [ - "/css/css-view-transitions/view-transition-types-one-green-square-ref.html", - "==" - ] - ], - {} - ] - ], "web-animations-api-parse-pseudo-argument.html": [ "dfaac62c17f99e7465a32c452eaed807f289cac6", [ @@ -291523,7 +291913,7 @@ ] ], "iframe-zoom-nested.html": [ - "22a491eb0b58e4cc908336597bdd75f88f5fa18b", + "9dc99a0fa55ea28e4a7e36ecd90166aef64771fb", [ null, [ @@ -291536,7 +291926,7 @@ ] ], "iframe-zoom.sub.html": [ - "a27fb91619b6b82290c3468dd1b966983b7287fa", + "3ddfcd820d72f3ecbc5365a303f9ce1311be60b1", [ null, [ @@ -302326,7 +302716,7 @@ ] ], "mongolian-orientation-002.html": [ - "66dde36ac09fd0ac28369593c789124dd858417e", + "82dabab9c2fd8eef8c1a1ebb11ddc67afdbddac3", [ null, [ @@ -318820,6 +319210,45 @@ {} ] ], + "2d.layer.non-invertible-matrix.html": [ + "0cd13b7c224778b4578b61bf5586b580ca3c2686", + [ + null, + [ + [ + "/html/canvas/element/layers/2d.layer.non-invertible-matrix-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.shadow.html": [ + "864935db63f900f0595e566e80fec74263ee70c6", + [ + null, + [ + [ + "/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.with-render-states-and-filter.html": [ + "c81732c1bd3c70addd2c812930141c084ea00d72", + [ + null, + [ + [ + "/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html", + "==" + ] + ], + {} + ] + ], "2d.layer.opaque-canvas.html": [ "be8b088fbbbd3b5778a032efe7ef0c3863537775", [ @@ -321570,6 +321999,84 @@ {} ] ], + "2d.layer.non-invertible-matrix.html": [ + "426268baea89416d9653f57904c6f4149f248aeb", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.shadow.html": [ + "45c20b527fb650860854b88c274ecde485875231", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.shadow.w.html": [ + "573168f7c422a4c050285d9637129a882fcab14c", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.w.html": [ + "19aafd6d1abe8e29401528c54cb49c336af3eb88", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.with-render-states-and-filter.html": [ + "ec69a96b70270b5fc6d34d9b05a0f1a15bf40d08", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html", + "==" + ] + ], + {} + ] + ], + "2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html": [ + "1fe9d99d6d7b56c4509a670124dc40e67cb99bf9", + [ + null, + [ + [ + "/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html", + "==" + ] + ], + {} + ] + ], "2d.layer.opaque-canvas.html": [ "f9759abebeb71ed5733e03be85d58412429a9e1e", [ @@ -340973,6 +341480,19 @@ {} ] ], + "text-font-face-load-image.html": [ + "eb9a5fe4357373e3be74293c66f14f621a911405", + [ + null, + [ + [ + "/svg/text/reftests/text-font-face-load-image-ref.html", + "==" + ] + ], + {} + ] + ], "text-inline-size-001.svg": [ "ae1a10d190ac0f04a2a3168ff8096aaa4fa60066", [ @@ -344867,7 +345387,7 @@ }, "support": { ".azure-pipelines.yml": [ - "c018c592d1db33277a6499a8a20bc0f304d221fd", + "2455ad8df121717ef12b224e8c944b3a460377e4", [] ], ".gitattributes": [ @@ -344885,7 +345405,7 @@ ], "workflows": { "docker.yml": [ - "655e881869d5561820a7b13b6a7d498056a65d0b", + "a55d0f626d26ab824a10eedaafb52af614eced2c", [] ], "documentation.yml": [ @@ -388805,6 +389325,10 @@ "rectangle-96x60.png": [ "add0d272b1eb56d0003d414c6e04f080053b62a4", [] + ], + "sprite-view-no-viewbox.svg": [ + "c17f5cf582611b0b85e5af0c1668e635865b057c", + [] ] }, "vector": { @@ -397610,6 +398134,14 @@ "deb5f9324a1cb7747919fdfcf7a347e32ee0b514", [] ], + "font-variant-emoji-003-ref.html": [ + "1e8cdb89cfa744b2f13ed7fc1d53ee06b8d735d9", + [] + ], + "font-variant-emoji-004-ref.html": [ + "b15c8227cc4a019c369c5c97b53bd6810eb0b72f", + [] + ], "font-variant-emoji-1-notref.html": [ "bbf3654ccfcdd415fe5a28468e3329ba94ce63da", [] @@ -397921,11 +398453,11 @@ [] ], "NotoColorEmoji-Regular_subset.ttf": [ - "24ab79fd05295151de61a16be460b4f8e7892f7c", + "8754e127d7fdecf4e13e1d8f4222d6b600fb66d2", [] ], "NotoEmoji-Regular_subset.ttf": [ - "0b054c7c8d597e6d600817ec05543d7612e79351", + "f9996293bf667c50c8a34a4a49c13064ee640db4", [] ], "NotoEmoji-Regular_without-cmap14-subset.ttf": [ @@ -407599,6 +408131,10 @@ "8c10a8037064a09d8a0c04357315ad8498e3a31d", [] ], + "text-box-trim-height-001-ref.html": [ + "9bd12cb0bdf9669d21e13ecec857a3bc31010add", + [] + ], "text-box-trim-initial-letter-end-001-ref.html": [ "f6f227e11d066f5bb036391dffd8a183a1acbe2c", [] @@ -407927,7 +408463,7 @@ [] ], "counter-set-001-ref.html": [ - "2b5eb90ccfc61af30e95e539a34f85ac8a8b58cd", + "301197904a5af79eb5024bb7d97ac161e5ada3ad", [] ], "counter-set-002-ref.html": [ @@ -407959,7 +408495,11 @@ [] ], "counters-005-ref.html": [ - "232bab76a106674358e024f9e0bb97993aac476a", + "509a097ef468844b54b76a8e9ab9c62d50f4e0be", + [] + ], + "counters-006-ref.html": [ + "8e21dca4cee41577555c6d54448e89656b9bb167", [] ], "counters-scope-001-ref.html": [ @@ -410256,6 +410796,258 @@ "131e54ec2913621bd0cd6e3adf8e22e03b129d8a", [] ], + "line-clamp": { + "reference": { + "line-clamp-010-ref.html": [ + "46ca731c5446db88ba4e5fc8a3d6b8b43c4fc056", + [] + ], + "line-clamp-011-ref.html": [ + "04297fff2b5538a8be020d780cba3d82aa30fa9e", + [] + ], + "line-clamp-012-ref.html": [ + "f412e0110d45ac2db2e8e55adfdab3ae9dce57df", + [] + ], + "line-clamp-013-ref.html": [ + "b2eb05e884d781ae9d5fdaa690e17be60607d97f", + [] + ], + "line-clamp-015-ref.html": [ + "1af45c1225c2df9ed258eb69c687f11c4be9a965", + [] + ], + "line-clamp-021-ref.html": [ + "d794c76e3c9653dd94b2bad73cdf2a4574db5f50", + [] + ], + "line-clamp-auto-002-ref.html": [ + "fe0a8dbd588a8a56c8ac0488713a061ef83474f9", + [] + ], + "line-clamp-auto-005-ref.html": [ + "01eea67a0da77dc7a99bcd0b8677abdd3a4033f5", + [] + ], + "line-clamp-auto-011-ref.html": [ + "5f7120ee39a2014f75f2f700ebedcc0b0e6e6275", + [] + ], + "line-clamp-auto-016-ref.html": [ + "4a5f3536cc62c33e1a30ed9fce9a21cecda341bc", + [] + ], + "line-clamp-with-abspos-001-ref.html": [ + "d756162dde0c54bd52646597b01bbff8a80f5fd8", + [] + ], + "line-clamp-with-abspos-005-ref.html": [ + "3b1f9218e887a7e50745f0c5e540c099475a59df", + [] + ], + "line-clamp-with-abspos-006-ref.html": [ + "4b55c37a033ea4d4a9d97b5fdf8035a60aace4bd", + [] + ], + "line-clamp-with-abspos-007-ref.html": [ + "e3dcc696e39684b47e1785968f29ec9adf8f9c0b", + [] + ], + "line-clamp-with-abspos-008-ref.html": [ + "373b2755c1d617e1716afe8060c28b246d52c33e", + [] + ], + "line-clamp-with-abspos-010-ref.html": [ + "ecc2fcee1b5219b310ef15b28f0684b07d84baf6", + [] + ], + "line-clamp-with-abspos-011-ref.html": [ + "f08b0270e8d7aa8a389910319617528dac6a4240", + [] + ], + "line-clamp-with-floats-001-ref.html": [ + "bcd70aa9305f73a44c46542208f0ad664c92bfde", + [] + ], + "line-clamp-with-floats-005-ref.html": [ + "d20d6c53ddee3de2fe12ea1927eb3182a0f55201", + [] + ], + "line-clamp-with-floats-006-ref.html": [ + "9288c4e36f924c4ab80bb9ad368b87a3a2d01720", + [] + ], + "line-clamp-with-floats-007-ref.html": [ + "6d5390246b4d28365a4e8f238bd02f4fe6be9c21", + [] + ], + "line-clamp-with-floats-008-ref.html": [ + "50b3d53900e21d7bf031fd25d69c80f174bca7c2", + [] + ], + "line-clamp-with-floats-010-ref.html": [ + "12b8cdc441a64fb4e1a6249ee8aea640c66b82b5", + [] + ], + "webkit-line-clamp-001-ref.html": [ + "ef28e01dac0223c9a2768d3fd1415fb9ef2f1c6b", + [] + ], + "webkit-line-clamp-005-ref.html": [ + "c9a9ae5d7ffebd9f041186a8d79f49f827becd61", + [] + ], + "webkit-line-clamp-006-ref.html": [ + "3a17a6d5f3b47c511578b28ffd9fc35b5b61dd98", + [] + ], + "webkit-line-clamp-007-ref.html": [ + "1c9a728aafcee7e830c203f3abec2393bd58e104", + [] + ], + "webkit-line-clamp-008-ref.html": [ + "4ead437830f96be87f75c687c973e21d63021d99", + [] + ], + "webkit-line-clamp-009-ref.html": [ + "62e100d63346b1da7d80ca34dc2fdf4bcd1972ab", + [] + ], + "webkit-line-clamp-010-ref.html": [ + "63e9885e8b9ce3cac850987917ca301045d4128f", + [] + ], + "webkit-line-clamp-011-ref.html": [ + "fbcd8ca3115e70ce2244a4b22820ede745e3bc09", + [] + ], + "webkit-line-clamp-012-ref.html": [ + "d28c9388aea4c494d1773dd2e1ffcbb7a7712bb3", + [] + ], + "webkit-line-clamp-013-ref.html": [ + "ba14aefc7cc3b27318325c22ef35cb1da755193b", + [] + ], + "webkit-line-clamp-014-ref.html": [ + "c7346ab527935246f795e1f0181abd7c31cd4fb7", + [] + ], + "webkit-line-clamp-015-ref.html": [ + "d73bc7d7cb6c54ea7d6141b04f901ef345c44e14", + [] + ], + "webkit-line-clamp-016-ref.html": [ + "e54947e3956cbe0acc1cab1fc91eef8763775c04", + [] + ], + "webkit-line-clamp-017-ref.html": [ + "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", + [] + ], + "webkit-line-clamp-018-ref.html": [ + "b95f7ce7662fac02eed3594f47ec42cca202cc26", + [] + ], + "webkit-line-clamp-019-ref.html": [ + "b95f7ce7662fac02eed3594f47ec42cca202cc26", + [] + ], + "webkit-line-clamp-020-ref.html": [ + "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", + [] + ], + "webkit-line-clamp-021-ref.html": [ + "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", + [] + ], + "webkit-line-clamp-022-ref.html": [ + "29d738589d19afef0d85a107cd803bdd05743116", + [] + ], + "webkit-line-clamp-023-ref.html": [ + "3127bb078b348f7604b421b28cad4dee13e8afb7", + [] + ], + "webkit-line-clamp-024-ref.html": [ + "d4820a1b487a031611b7010a3513fb3daf06d71f", + [] + ], + "webkit-line-clamp-025-ref.html": [ + "c33a6b5033890c8a27d5de58a801a80a3083b554", + [] + ], + "webkit-line-clamp-026-ref.html": [ + "e4c1b033499cbea1ba657009ae86d1f4366df3d8", + [] + ], + "webkit-line-clamp-027-ref.html": [ + "be27d6d276abb1c910c3c1c3dab00128e6c0c2c2", + [] + ], + "webkit-line-clamp-029-ref.html": [ + "0d18c2a7f55f4f071657ee3d3c4e610f7e8fb05b", + [] + ], + "webkit-line-clamp-030-ref.html": [ + "e88c8bffc02da9e187ba6f30834751e84b590940", + [] + ], + "webkit-line-clamp-031-ref.html": [ + "14f2fb1bb40f2885a304b0e9de8a866027e689c1", + [] + ], + "webkit-line-clamp-032-ref.html": [ + "8f5b1ef53eb72dd82f1211744a21c34dede17b46", + [] + ], + "webkit-line-clamp-035-ref.html": [ + "230218399e31f9dc79ba6aef2856a16be1ae98b4", + [] + ], + "webkit-line-clamp-036-ref.html": [ + "0de35e98ab353519664996fba8ab8ef6916a18d0", + [] + ], + "webkit-line-clamp-037-ref.html": [ + "2927716d76dbc8ebc8b98169c533301214a517aa", + [] + ], + "webkit-line-clamp-038-ref.html": [ + "bd8db762dd889c65d55a31e33ce9913a125fff3a", + [] + ], + "webkit-line-clamp-039-ref.html": [ + "fd8a76b2f8e67a901500dd0ae26074f2404518a0", + [] + ], + "webkit-line-clamp-040-ref.html": [ + "f55be86e5461dfc1d302d0649f2aa0d31a285b8b", + [] + ], + "webkit-line-clamp-block-in-inline-001-ref.html": [ + "79f2e409109d72a76e12374220bd423aba16f4eb", + [] + ], + "webkit-line-clamp-dynamic-001-ref.html": [ + "21458953df99e0be27d2143c182a5829438ede5d", + [] + ], + "webkit-line-clamp-with-line-height-ref.html": [ + "c2c8914161af668cd57ce7ebac15b2100d93be8b", + [] + ] + } + }, + "overflow-alignment-001-ref.html": [ + "5a89c88467eccda859bbec0c09d421afd2a61613", + [] + ], + "overflow-alignment-002-ref.html": [ + "8c2974347515641ab47d04ef63da9b4615302ef7", + [] + ], "overflow-auto-scrollbar-gutter-intrinsic-001-ref.html": [ "8c092bd0c876bc328919f4b39358110dd902c726", [] @@ -410447,98 +411239,6 @@ "d7125ee2ef3285d461b2172208e23d8d4cc27a64", [] ], - "line-clamp-010-ref.html": [ - "46ca731c5446db88ba4e5fc8a3d6b8b43c4fc056", - [] - ], - "line-clamp-011-ref.html": [ - "04297fff2b5538a8be020d780cba3d82aa30fa9e", - [] - ], - "line-clamp-012-ref.html": [ - "f412e0110d45ac2db2e8e55adfdab3ae9dce57df", - [] - ], - "line-clamp-013-ref.html": [ - "b2eb05e884d781ae9d5fdaa690e17be60607d97f", - [] - ], - "line-clamp-015-ref.html": [ - "1af45c1225c2df9ed258eb69c687f11c4be9a965", - [] - ], - "line-clamp-021-ref.html": [ - "d794c76e3c9653dd94b2bad73cdf2a4574db5f50", - [] - ], - "line-clamp-auto-002-ref.html": [ - "fe0a8dbd588a8a56c8ac0488713a061ef83474f9", - [] - ], - "line-clamp-auto-005-ref.html": [ - "01eea67a0da77dc7a99bcd0b8677abdd3a4033f5", - [] - ], - "line-clamp-auto-011-ref.html": [ - "5f7120ee39a2014f75f2f700ebedcc0b0e6e6275", - [] - ], - "line-clamp-auto-016-ref.html": [ - "4a5f3536cc62c33e1a30ed9fce9a21cecda341bc", - [] - ], - "line-clamp-with-abspos-001-ref.html": [ - "d756162dde0c54bd52646597b01bbff8a80f5fd8", - [] - ], - "line-clamp-with-abspos-005-ref.html": [ - "3b1f9218e887a7e50745f0c5e540c099475a59df", - [] - ], - "line-clamp-with-abspos-006-ref.html": [ - "4b55c37a033ea4d4a9d97b5fdf8035a60aace4bd", - [] - ], - "line-clamp-with-abspos-007-ref.html": [ - "e3dcc696e39684b47e1785968f29ec9adf8f9c0b", - [] - ], - "line-clamp-with-abspos-008-ref.html": [ - "373b2755c1d617e1716afe8060c28b246d52c33e", - [] - ], - "line-clamp-with-abspos-010-ref.html": [ - "ecc2fcee1b5219b310ef15b28f0684b07d84baf6", - [] - ], - "line-clamp-with-abspos-011-ref.html": [ - "f08b0270e8d7aa8a389910319617528dac6a4240", - [] - ], - "line-clamp-with-floats-001-ref.html": [ - "bcd70aa9305f73a44c46542208f0ad664c92bfde", - [] - ], - "line-clamp-with-floats-005-ref.html": [ - "d20d6c53ddee3de2fe12ea1927eb3182a0f55201", - [] - ], - "line-clamp-with-floats-006-ref.html": [ - "9288c4e36f924c4ab80bb9ad368b87a3a2d01720", - [] - ], - "line-clamp-with-floats-007-ref.html": [ - "6d5390246b4d28365a4e8f238bd02f4fe6be9c21", - [] - ], - "line-clamp-with-floats-008-ref.html": [ - "50b3d53900e21d7bf031fd25d69c80f174bca7c2", - [] - ], - "line-clamp-with-floats-010-ref.html": [ - "12b8cdc441a64fb4e1a6249ee8aea640c66b82b5", - [] - ], "overflow-body-no-propagation-ref.html": [ "9795d1f5861f0affaeb3a36b3644d17fd60e1f4d", [] @@ -410598,154 +411298,6 @@ "text-overflow-scroll-vertical-lr-rtl-001-ref.html": [ "fff3dec59cb494d98ee87660ded2b9ee05a5e198", [] - ], - "webkit-line-clamp-001-ref.html": [ - "ef28e01dac0223c9a2768d3fd1415fb9ef2f1c6b", - [] - ], - "webkit-line-clamp-005-ref.html": [ - "c9a9ae5d7ffebd9f041186a8d79f49f827becd61", - [] - ], - "webkit-line-clamp-006-ref.html": [ - "3a17a6d5f3b47c511578b28ffd9fc35b5b61dd98", - [] - ], - "webkit-line-clamp-007-ref.html": [ - "1c9a728aafcee7e830c203f3abec2393bd58e104", - [] - ], - "webkit-line-clamp-008-ref.html": [ - "4ead437830f96be87f75c687c973e21d63021d99", - [] - ], - "webkit-line-clamp-009-ref.html": [ - "62e100d63346b1da7d80ca34dc2fdf4bcd1972ab", - [] - ], - "webkit-line-clamp-010-ref.html": [ - "63e9885e8b9ce3cac850987917ca301045d4128f", - [] - ], - "webkit-line-clamp-011-ref.html": [ - "fbcd8ca3115e70ce2244a4b22820ede745e3bc09", - [] - ], - "webkit-line-clamp-012-ref.html": [ - "d28c9388aea4c494d1773dd2e1ffcbb7a7712bb3", - [] - ], - "webkit-line-clamp-013-ref.html": [ - "ba14aefc7cc3b27318325c22ef35cb1da755193b", - [] - ], - "webkit-line-clamp-014-ref.html": [ - "c7346ab527935246f795e1f0181abd7c31cd4fb7", - [] - ], - "webkit-line-clamp-015-ref.html": [ - "d73bc7d7cb6c54ea7d6141b04f901ef345c44e14", - [] - ], - "webkit-line-clamp-016-ref.html": [ - "e54947e3956cbe0acc1cab1fc91eef8763775c04", - [] - ], - "webkit-line-clamp-017-ref.html": [ - "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", - [] - ], - "webkit-line-clamp-018-ref.html": [ - "b95f7ce7662fac02eed3594f47ec42cca202cc26", - [] - ], - "webkit-line-clamp-019-ref.html": [ - "b95f7ce7662fac02eed3594f47ec42cca202cc26", - [] - ], - "webkit-line-clamp-020-ref.html": [ - "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", - [] - ], - "webkit-line-clamp-021-ref.html": [ - "043e5f3d1c31ac0bc1e1db014769e7dedd3f1f68", - [] - ], - "webkit-line-clamp-022-ref.html": [ - "29d738589d19afef0d85a107cd803bdd05743116", - [] - ], - "webkit-line-clamp-023-ref.html": [ - "3127bb078b348f7604b421b28cad4dee13e8afb7", - [] - ], - "webkit-line-clamp-024-ref.html": [ - "d4820a1b487a031611b7010a3513fb3daf06d71f", - [] - ], - "webkit-line-clamp-025-ref.html": [ - "c33a6b5033890c8a27d5de58a801a80a3083b554", - [] - ], - "webkit-line-clamp-026-ref.html": [ - "e4c1b033499cbea1ba657009ae86d1f4366df3d8", - [] - ], - "webkit-line-clamp-027-ref.html": [ - "be27d6d276abb1c910c3c1c3dab00128e6c0c2c2", - [] - ], - "webkit-line-clamp-029-ref.html": [ - "0d18c2a7f55f4f071657ee3d3c4e610f7e8fb05b", - [] - ], - "webkit-line-clamp-030-ref.html": [ - "e88c8bffc02da9e187ba6f30834751e84b590940", - [] - ], - "webkit-line-clamp-031-ref.html": [ - "14f2fb1bb40f2885a304b0e9de8a866027e689c1", - [] - ], - "webkit-line-clamp-032-ref.html": [ - "8f5b1ef53eb72dd82f1211744a21c34dede17b46", - [] - ], - "webkit-line-clamp-035-ref.html": [ - "230218399e31f9dc79ba6aef2856a16be1ae98b4", - [] - ], - "webkit-line-clamp-036-ref.html": [ - "0de35e98ab353519664996fba8ab8ef6916a18d0", - [] - ], - "webkit-line-clamp-037-ref.html": [ - "2927716d76dbc8ebc8b98169c533301214a517aa", - [] - ], - "webkit-line-clamp-038-ref.html": [ - "bd8db762dd889c65d55a31e33ce9913a125fff3a", - [] - ], - "webkit-line-clamp-039-ref.html": [ - "fd8a76b2f8e67a901500dd0ae26074f2404518a0", - [] - ], - "webkit-line-clamp-040-ref.html": [ - "f55be86e5461dfc1d302d0649f2aa0d31a285b8b", - [] - ], - "webkit-line-clamp-block-in-inline-001-ref.html": [ - "79f2e409109d72a76e12374220bd423aba16f4eb", - [] - ], - "webkit-line-clamp-dynamic-001-ref.html": [ - "21458953df99e0be27d2143c182a5829438ede5d", - [] - ], - "webkit-line-clamp-with-line-height-ref.html": [ - "c2c8914161af668cd57ce7ebac15b2100d93be8b", - [] ] }, "rounded-overflow-clip-visible-ref.html": [ @@ -411000,6 +411552,10 @@ "d8468e3b14dc0346f178727336572f5122b0e4bf", [] ], + "page-background-image-print-ref.html": [ + "f38cc89838027f5f862cbd08459c1694f4a30e01", + [] + ], "page-box-000-print-ref.html": [ "d39bd738507490b5590afa7a4eb6cce9766b63bc", [] @@ -412316,6 +412872,10 @@ "82c5074fd26aa2dc35c9c8fa77dd3bf1099c5f88", [] ], + "first-letter-width-2-ref.html": [ + "67fe00ca79030554f19be7100ccb505dc273e300", + [] + ], "first-letter-width-ref.html": [ "8ebb00b9dcc10f43df0efea20991af653f5cb691", [] @@ -413062,10 +413622,18 @@ "9b513c9adaed3cb7a5d00057a7642d0776cb6163", [] ], + "interlinear-block-margin-box-ref.html": [ + "c9839d94cf27547190a66c2de5f244385c159895", + [] + ], "nested-ruby-pairing-001-ref.html": [ "8c470232876d5a3d3b0c988f2cfef0936d142a10", [] ], + "pseudo-first-letter-ref.html": [ + "3834976b81367aa7a15bf65cb5c6351e843cdfc8", + [] + ], "pseudo-first-line-ref.html": [ "db20b5032757e178fa4ec3a5a650dfc1dadfb28c", [] @@ -424396,6 +424964,10 @@ "14b76fb07e76189306ffd148886617bec1cf0100", [] ], + "inline-with-offset-from-containing-block-clipped-ref.html": [ + "2b6b56578c23a41c33484ddbd253859af3c02115", + [] + ], "inline-with-offset-from-containing-block-ref.html": [ "4a66af4ece26abd69a41840319783f86001746d0", [] @@ -424608,6 +425180,10 @@ "79e89801391530b6fb074545a92db68493667f05", [] ], + "new-content-ancestor-clipped-2-ref.html": [ + "f37c9544be60183e8ae831c413330f34d4cfb757", + [] + ], "new-content-ancestor-clipped-ref.html": [ "caa99f280796f2ccae9b926f14f8355599f02add", [] @@ -424652,6 +425228,10 @@ "9ed3f5f681c4c86cb5397cc25d66f3dd8b68902f", [] ], + "new-content-flat-transform-ancestor-ref.html": [ + "8728d34ae49502881159ff16b7bdd497faca6283", + [] + ], "new-content-from-root-display-none-ref.html": [ "6c165d1b24415a791848788d2b3b2c08683f4902", [] @@ -424684,10 +425264,18 @@ "89d23319710d00a46f29da79b15976641a72e171", [] ], + "new-content-preserve-3d-ancestor-ref.html": [ + "c6c054c130be55d5886264979d0f2364901287ac", + [] + ], "new-content-scaling-ref.html": [ "729be2fcbf606723a4e3449bf3ffad97c8e6bf15", [] ], + "new-content-transform-position-fixed-ref.html": [ + "3c3a33d0b2ce23eeffc1dee733c12bf11e09cdb9", + [] + ], "new-element-on-start-ref.html": [ "8c096d1b10e42bdb9c7e5582affd225afdb6c9f1", [] @@ -424696,6 +425284,10 @@ "99ca705d00e3d6e9b1a5d26c1cf2477a2d8c9d7e", [] ], + "no-painting-while-render-blocked-ref.html": [ + "8c7ab1bc5ffee13ff74e7bf33532145180b50e70", + [] + ], "no-root-capture-ref.html": [ "1fe68ad941e111f74aa5b71828c95050d6c33d44", [] @@ -424788,12 +425380,16 @@ "89b3a8a78efda6184ad62088c3b7595cc4f6d829", [] ], + "reset-state-after-scrolled-view-transition-ref.html": [ + "93cb9975d4c6889c383330048b5cf7c7d75e3b2e", + [] + ], "root-captured-as-different-tag-ref.html": [ "ca4705f818461c3df509adaa295318fe36b86a80", [] ], "root-scrollbar-with-fixed-background-ref.html": [ - "7b2a6103f3bf27494089f9d8b210d9f99e7f959c", + "ae1ff38f2f63dae173043e9ae07a7e494e236134", [] ], "root-style-change-during-animation-ref.html": [ @@ -424971,11 +425567,11 @@ ], "reference": { "iframe-zoom-nested-ref.html": [ - "b855278516cb03d0b5ef7921a3fded41f28f684f", + "fcdc16b1fb784f829bd1d964f04987737abfe02f", [] ], "iframe-zoom-ref.html": [ - "43bc3e24cf832079f56772747f4b493c121a7c59", + "f482d29ebf2900dad11ed87f40bff5edff270b19", [] ], "letter-spacing-ref.html": [ @@ -424992,6 +425588,10 @@ "58c4d03a466e2bb045da142e354c565ec122b502", [] ], + "leaf.html": [ + "21e75b62768e660ad60a004b411d71821ddcb014", + [] + ], "nested-iframe-no-zoom.html": [ "60b1fd648155eb1188ceb4595f79bca14a45660d", [] @@ -424999,6 +425599,10 @@ "nested-iframe-with-zoom.html": [ "e7de64aafb769e106885ea2b4e01fb75331c3e68", [] + ], + "nested-iframe.html": [ + "f48d230e13d7833ec204ec2382f5a2deb927da1b", + [] ] }, "svg-ref.html": [ @@ -430420,12 +431024,12 @@ [] ], "support": { - "digital-identity-helper.js": [ + "helper.js": [ "8fff82745172154972dcda4c3c6cb98d4a5af5e1", [] ], - "digital-identity-iframe.html": [ - "8b3a424d1e21d381b76849deb261762aec016298", + "iframe.html": [ + "74733d82ec6533cbd0c6da4af1e88af61413e868", [] ] } @@ -431328,7 +431932,7 @@ "parts": { "resources": { "domparts-utils.js": [ - "5deeec80a358a56e2c40c2b6e371591210504316", + "f8982de50f334fa7899385b27085434e4f97a6a7", [] ] } @@ -437620,7 +438224,7 @@ ] } }, - "geolocation-API": { + "geolocation": { "META.yml": [ "e3d6c4dff8eddcd9edfa0a56e4efb87cb08e8453", [] @@ -440710,6 +441314,18 @@ "8b53e2dc7619126ffa3e5aa8cb3a24497c0b5383", [] ], + "2d.layer.non-invertible-matrix-expected.html": [ + "0d61a3e6091a8596535b6f8ed3acc6f28b8601fa", + [] + ], + "2d.layer.non-invertible-matrix.shadow-expected.html": [ + "0cb7e929272ba0e974492eeb4a64cf731efc77e0", + [] + ], + "2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html": [ + "10bfc7a19b36ba1b3af4b47ec033caf85fc8c902", + [] + ], "2d.layer.opaque-canvas-expected.html": [ "89c85de1e5cbad6bccef23a88e9de78de94293c4", [] @@ -441778,6 +442394,18 @@ "8b53e2dc7619126ffa3e5aa8cb3a24497c0b5383", [] ], + "2d.layer.non-invertible-matrix-expected.html": [ + "0d61a3e6091a8596535b6f8ed3acc6f28b8601fa", + [] + ], + "2d.layer.non-invertible-matrix.shadow-expected.html": [ + "0cb7e929272ba0e974492eeb4a64cf731efc77e0", + [] + ], + "2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html": [ + "10bfc7a19b36ba1b3af4b47ec033caf85fc8c902", + [] + ], "2d.layer.opaque-canvas-expected.html": [ "89c85de1e5cbad6bccef23a88e9de78de94293c4", [] @@ -442032,7 +442660,7 @@ [] ], "gentestutilsunion.py": [ - "2880240799501d0b55c79c91809da8f75a3d702f", + "ab6f4e1cd08145470f7bb8c6d0a477e06b63713a", [] ], "name2dir-canvas.yaml": [ @@ -442161,7 +442789,7 @@ [] ], "layers.yaml": [ - "a915f391ec239578abeb532923e3cc9cbdf78576", + "59030a69914b0c6556b9cbef50f07601414921ed", [] ], "line-styles.yaml": [ @@ -450083,7 +450711,7 @@ "stylable-select": { "resources": { "stylable-select-styles.css": [ - "7f4c8b6c448af5ac01bdf85c906ecd8bc18bff79", + "0052b3863e2ef5de0048968e11312bba631f1264", [] ], "stylable-select-utils.js": [ @@ -450092,7 +450720,7 @@ ] }, "select-child-button-and-datalist-ref.html": [ - "e99ca4d57a8b2e3ea5f7e208e90a2a710710faf8", + "a27e662b1c4caba8633c807cb6051dd978415e02", [] ], "select-icon-color-ref.html": [ @@ -450100,7 +450728,7 @@ [] ], "select-open-invalidation-ref.html": [ - "f5b70c495bc82a0f81c2898067232ddcb6ddb162", + "fb609a7247e6413967c64c4017ade1970da184da", [] ] } @@ -455580,7 +456208,7 @@ [] ], "FileAPI.idl": [ - "aee0e65dcae267201bbe6ae8b8c90231795facf1", + "49219fce277f2387d0d7ed39cf87d95da497f0f6", [] ], "IndexedDB.idl": [ @@ -455792,7 +456420,7 @@ [] ], "compression.idl": [ - "7525d7c9847b74a8e382e55ad9627b64dd4af161", + "defe4ba55cdfd4154e7472181391f52fe83fbb82", [] ], "compute-pressure.idl": [ @@ -455820,7 +456448,7 @@ [] ], "credential-management.idl": [ - "75e18319190a7976379b8b5ec5d153270c1fa8ad", + "94cf6a58614f4ba14b114fd844a4dc048c6493e8", [] ], "csp-embedded-enforcement.idl": [ @@ -455956,7 +456584,7 @@ [] ], "cssom-view.idl": [ - "5c6c2f58a8c1a05b6436ab995f0e703d6f30263b", + "f922bc81486442108d566645b31c4dd24f257b5b", [] ], "cssom.idl": [ @@ -456032,7 +456660,7 @@ [] ], "fenced-frame.idl": [ - "9846d4037e934f8f7927abfbefd0fdf61410bc24", + "a3ec8d731bbcd65491ff533322994f45c5641fab", [] ], "fetch.idl": [ @@ -456104,7 +456732,7 @@ [] ], "html.idl": [ - "aad8994b87dae6df4134353fffa68416c0b9eb4c", + "b5bf357357946ba8255dc3b309409602997147f4", [] ], "idle-detection.idl": [ @@ -456232,7 +456860,7 @@ [] ], "mediacapture-transform.idl": [ - "5b2c8fa67a6b130d1fe99f962ff85c5cf7a7bc3d", + "1ce35452f0c2afc7762ca53b6d83199a638e5a91", [] ], "mediacapture-viewport.idl": [ @@ -456240,7 +456868,7 @@ [] ], "mediasession.idl": [ - "e6c8e46462793cf6e96bebe48c04c466f8cc5117", + "0630f48da4b29f25be358c1cf9fe9d589051dd67", [] ], "mediastream-recording.idl": [ @@ -456368,7 +456996,7 @@ [] ], "push-api.idl": [ - "f582788806c79223f9a7b935ce48c179ac247cd9", + "b16a730b7229d02bb8c06434b521815ae3440d78", [] ], "raw-camera-access.idl": [ @@ -456407,6 +457035,10 @@ "33fed05b75683891047785f7521b7a250086c645", [] ], + "saa-non-cookie-storage.idl": [ + "6cc0a7887be78013f74c80ed0b1ab83cc39647ef", + [] + ], "sanitizer-api.idl": [ "8f5c667973a359ed576c143b0a5aa639c0a89b94", [] @@ -456420,7 +457052,7 @@ [] ], "scheduling-apis.idl": [ - "1e84e79cd1503946088888d02a5da45d5af7c5fe", + "3c858a2db06ad3f7e47198aaa4935303116533fe", [] ], "screen-capture.idl": [ @@ -456516,7 +457148,7 @@ [] ], "turtledove.idl": [ - "fdaef0268b6f4554680e82adaf9fd72b75064d94", + "d4cc8c6bc5d5b106dc3f495058a2108ed6792c6f", [] ], "ua-client-hints.idl": [ @@ -456660,7 +457292,7 @@ [] ], "webgpu.idl": [ - "ef5b9c730abed4a31306cfb36d9a68c51ac91528", + "4a1b339a002661035a8b5f366d6ce166b8bfe37c", [] ], "webhid.idl": [ @@ -456676,7 +457308,7 @@ [] ], "webnn.idl": [ - "297a2bdbf873b2a7f44be847faffdb86679b7f06", + "29c88122adc760aa49d431057f7c26d5b426440f", [] ], "webrtc-encoded-transform.idl": [ @@ -456704,7 +457336,7 @@ [] ], "webrtc.idl": [ - "da3a6f4645e082e66719bcc573fa22ad13708179", + "de6ba1420cec4bb53b1651cea69ac030db6f4a78", [] ], "websockets.idl": [ @@ -456732,7 +457364,7 @@ [] ], "webxr-depth-sensing.idl": [ - "c44f029436f11afe8fe91023c3eda20af3f5755b", + "b77b59c0ce67425d459428721c92d3f548f10048", [] ], "webxr-dom-overlays.idl": [ @@ -457165,7 +457797,7 @@ ] }, "lint.ignore": [ - "31ce1ab3a96d3e9184dcdafcb8fa211f80bd845d", + "170f4136d4b4f60827f37965152be573671d28fc", [] ], "loading": { @@ -461027,7 +461659,7 @@ [] ], "permissions-policy.js": [ - "d30d1191d169d5a7bb51c6fdbfb232b06e2be804", + "d700cb086b362229e194cd2596f151bdf74d0039", [] ], "picture-in-picture.js": [ @@ -461727,6 +462359,14 @@ } }, "private-aggregation": { + "private-aggregation-permissions-policy-none.https.sub.html.headers": [ + "878191015163abd568cff4664c0b7dbe0b2239a6", + [] + ], + "private-aggregation-permissions-policy-self.https.sub.html.headers": [ + "c4f9fe899effc3d875997e61657c351c1821c09c", + [] + ], "resources": { "protected-audience-helper-module.js": [ "be4f01379e236938c255a366afca97bdc2e79675", @@ -461744,15 +462384,15 @@ "f5a8533d0f384391785f8f1af0fe29782296f081", [] ], + "shared-storage-helper-module.js.headers": [ + "cf3e03e24c7d68e3fb9f0be9102591c659a2b43c", + [] + ], "util.js": [ - "24e156446f140e460bf1dc701d37383cb754b0cf", + "a4e7e905c65f0f6640c9e61bec9c9169a77f57c7", [] ] - }, - "shared-storage-permissions-policy-none.https.html.headers": [ - "878191015163abd568cff4664c0b7dbe0b2239a6", - [] - ] + } }, "proximity": { "META.yml": [ @@ -464107,7 +464747,7 @@ [] ], "conftest.py": [ - "7253cac9acf053720dd701e7d30d6b3633dfb9c8", + "723087d31841b831b598edcd2cf7bc7cf290f495", [] ], "harness.html": [ @@ -468191,7 +468831,7 @@ [] ], "select-url-permissions-policy-helper.html": [ - "b70d763ad457415dd7facc78a003e5a20a1d9c5d", + "896daa59828a485908b4a8bf826ec10b00a2ae2b", [] ], "sender0.html": [ @@ -468207,7 +468847,7 @@ [] ], "shared-storage-permissions-policy-helper.html": [ - "d87092aad1d2efedf5d4245884e28e159880b2df", + "dd1bf50cf42c0ad46d3544e679d8f8a3737acbbb", [] ], "shared-storage-writable-fetch-request-fallback-to-network-iframe.https.html": [ @@ -468271,7 +468911,7 @@ [] ], "util.js": [ - "4a7fcc4590ff53e41695e9a6c11304e287eb3384", + "09eb45c591b42b3bbb969948362668daf902b3f0", [] ], "util.sub.js": [ @@ -468911,6 +469551,14 @@ "c14df771544e34d75d42118f306e164300e239c2", [] ], + "media-capabilities-decoding-info.https.html": [ + "5f1523701ea5621abed5f50ed9f5b8c3bb02f0e8", + [] + ], + "media-capabilities-encoding-info.https.html": [ + "539867824ce845a17d47daf02e085cd475fa878a", + [] + ], "media-device-info.https.html": [ "14d2ff78c99a3653db35546ffb08fe7741ee44be", [] @@ -469212,6 +469860,10 @@ [] ], "resources": { + "helpers.js": [ + "2dc5ab00dd16f181f85c4b0ab8f87934a932091d", + [] + ], "partitioned-estimate-usage-details-caches-helper-frame.html": [ "0679c1decf55835a0de37a9e64f12647da2a6c0a", [] @@ -470523,6 +471175,14 @@ "3d3968fa4923436e760dd80b05af292a615cffcb", [] ], + "text-font-face-load-image-ref.html": [ + "5a668b313e5eced93cc0358d84a75a56f5b76e8c", + [] + ], + "text-font-face-load-image-svg.svg": [ + "536938290f13d353367b2e98083940cd492766fe", + [] + ], "text-inline-size-001-ref.svg": [ "6abd211584ea3b500e409c0f0fa956182fe131e6", [] @@ -470964,7 +471624,7 @@ ] }, "conftest.py": [ - "8d1f585b0d8c347518a5a68ceca2bbcc553808c9", + "56521819507c13277a6420be01b3111582a0e6aa", [] ], "docker": { @@ -470993,7 +471653,7 @@ [] ], "requirements.txt": [ - "332fdbdeb1b12ee21595aeb21f1dade3f459115b", + "6451b3a800432a5272bcbc635d34864455101618", [] ], "retry.py": [ @@ -471262,7 +471922,7 @@ } }, "localpaths.py": [ - "a027af852df329cbb1c881d1ba6c4682b71eeeed", + "ebd3c54e7758b04e6ae45c1c3d20c3c93ef95b4e", [] ], "manifest": { @@ -471468,7 +472128,7 @@ [] ], "requirements_pytest.txt": [ - "64d38583a2fd682ae5d12ec0eac8e9f0711bc1af", + "75d70d49bd8006c97855a3528418c033c965c626", [] ], "requirements_tests.txt": [ @@ -471627,78 +472287,98 @@ ] }, "attrs": { + ".git_archival.txt": [ + "8fb235d7045be0330d94bcb3abd2ac43badaa197", + [] + ], + ".gitattributes": [ + "ec8c33334fee0c7b5da22af531a3394e02f63023", + [] + ], ".github": { "CODE_OF_CONDUCT.md": [ "1d8ad1833e7e9925d16e89efe73a7c31ec1d5ca0", [] ], "CONTRIBUTING.md": [ - "bbdc20f19324257c85e92350873ee6cf8b77b612", + "a7e5b014d33886e399be1ec9f78cbb128bd3f3ab", [] ], "FUNDING.yml": [ - "ef4f21216256d669a066c26e5903ffb1f1098ce1", + "7c250da1e51bb232bc1587ff058da78cde64d5c5", [] ], "PULL_REQUEST_TEMPLATE.md": [ - "88f6415e96cfb8d1777bc4f59d1a64c494cfdf9e", + "e84b6c86ac84fdee3e563e2d092845373fd22b9a", [] ], "SECURITY.md": [ - "5e565ec19cdd19aaec044f13efaa7a8530eda4c9", + "1b8e14cf1efbe8995fec57659a14e6dc0d012546", + [] + ], + "dependabot.yml": [ + "fd898955fba0748b44d8c7afbb6284bde672dd20", [] ], "workflows": { - "main.yml": [ - "f38fd915096704927027bd722538f967c8baa726", + "build-docset.yml": [ + "ec0230d080c7932286e5842019d2b06e01f12847", + [] + ], + "ci.yml": [ + "ca816aa601cbeaa6fa1cb4766f279e3a4858bb7d", + [] + ], + "codeql-analysis.yml": [ + "f75fafa5be3438d4b3860f2654d6062784f9ff4d", + [] + ], + "pypi-package.yml": [ + "8495480c1bcb5fd0ae8b9303795199cae07ccfea", [] ] } }, ".gitignore": [ - "d054dc6267d937e13fab0000e1a89b79ff26b1dc", + "b58afd704e5eb108377201fd919117e9fa847a8e", [] ], ".pre-commit-config.yaml": [ - "a913b068f52ebd954a7d777f14836d5c87535d00", + "df18314431670c9e8926ff5b8f6ccd53a9e13b63", [] ], - ".readthedocs.yml": [ - "d335c40d56765c37c94c63ca1078e4d80f78f76a", + ".python-version-default": [ + "e4fba2183587225f216eeada4c78dfab6b2e65f5", [] ], - "AUTHORS.rst": [ - "f14ef6c60749458fcb60bfdee27efe4d957b4aeb", + ".readthedocs.yaml": [ + "53bc38f7ee3a10900df85ff491fe311fb49b2be0", [] ], - "CHANGELOG.rst": [ - "1d194add22ba2f09b2150725cef14e117a548be3", + "CHANGELOG.md": [ + "a768197ae1d815374c476f5d27cd6229107debab", [] ], - "LICENSE": [ - "7ae3df930976bd01d34041b1c7ceeb4b32aace8c", + "CITATION.cff": [ + "83718ad889d76d57bd2e54ecf08b512b0fae8e34", [] ], - "MANIFEST.in": [ - "3d68bf9c5d5a95827e6a4b36039c7fedd61c392a", + "LICENSE": [ + "2bd6453d255e19b973f19b128596a8b6dd65b2c3", [] ], - "README.rst": [ - "709bba83d7e037c062b3ff02e308961db4e0a864", + "README.md": [ + "6ef8c0204eaca07f455839563860984a8929d2b5", [] ], "changelog.d": { - ".gitignore": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "towncrier_template.rst": [ - "29ca74c4e8f25fda7e77f0e6c4173dcb905f2f4b", + "towncrier_template.md.jinja": [ + "d9ae7c10efc162f87dde182ffffe2d515201ddb5", [] ] }, "conftest.py": [ - "0d539a115c77d2ab9fdd01d90edc0faca7ae4a03", + "144e5f3e19f8fd05534ec4552ec53f7aa572256f", [] ], "docs": { @@ -471722,133 +472402,159 @@ "attrs_logo_white.svg": [ "daad798da0080bf91b3221dcd7fbf496e6206c61", [] - ] + ], + "custom.css": [ + "72083fee482321b1b3501a1c4edbfed3cae3d4e5", + [] + ], + "docset-icon.png": [ + "d9886f1f6f6bc39913ac3c054c5d0fc93a2b6115", + [] + ], + "docset-icon@2x.png": [ + "5551b42bd92cbdec895a6b857445d229cb099308", + [] + ], + "social card.afdesign": [ + "06fa6696059d132ae5635e4d6d2e3853faf4504c", + [] + ], + "social card.png": [ + "46af5dcfd691cea269c52cf4cbc99836eea1bed9", + [] + ], + "sponsors": { + "FilePreviews.svg": [ + "2fff3aa3aeaefe12fa4e9ac630e50d9ec55ada98", + [] + ], + "Tidelift.svg": [ + "8b4da42cb1572370d89c9158c1074d00662a02d4", + [] + ], + "Variomedia.svg": [ + "90e750d25745672498c3603b0d05e9c765d6bea8", + [] + ] + } }, - "api.rst": [ - "02aed52ad5dfeeed0a02598933697bcc221e6f53", + "api-attr.rst": [ + "1c1c3edb3fcd6a7c798111efc4a2bd66004c12e2", [] ], - "changelog.rst": [ - "565b0521d0c2cdbfe493d19765b04d5c119fa7eb", + "api.rst": [ + "d55f2539ea5da9105f64da1657189f70744318e9", [] ], - "comparison.rst": [ - "760124ca3bc17982c1e9051ff4b07f21455428f8", + "changelog.md": [ + "3c8d4d8b3568275687fa3203f5add3ec7eec3be8", [] ], - "conf.py": [ - "0cc80be6a6857e9dbb9d58454ebbe4ade93549ff", + "comparison.md": [ + "79786e9e19ff20988f015ce78aafccb09c1aefee", [] ], - "docutils.conf": [ - "db8ca82c747cede027c01b3e9375d06d3954149d", - [] - ], - "examples.rst": [ - "ba5343d4ad20ceeadfcf8df68864fe0a487694b0", + "conf.py": [ + "b92354a6fd47044c9791d7728b93d4b7509ebfb8", [] ], - "extending.rst": [ - "faf71afd913d8c51a79c793b8d2c4ff770efce69", + "examples.md": [ + "0f8301aa59700a171ab5ebb034fc27d063c8d8c8", [] ], - "glossary.rst": [ - "5fd01f4fb19e4ca13a70227ea490ee043a51f2b0", + "extending.md": [ + "c6cb5f574bc94673052f30d3e4a373616fed86f8", [] ], - "hashing.rst": [ - "30888f97bba4e4e9448cda83ca73031deb6fb04f", + "glossary.md": [ + "6b09a3ad4d12abdf727c097568f4a8a2577dc39c", [] ], - "how-does-it-work.rst": [ - "f899740542cd1f6fa745117435a83d5ee019cc3d", + "hashing.md": [ + "231d818af6a1c4d42e0adf6cb87becc1e1a88124", [] ], - "index.rst": [ - "ff65a6738c9c0b1a33410276139752a857a32cbd", + "how-does-it-work.md": [ + "7acc8121322b2ba3e425a0b5fce4676796ae6e61", [] ], - "init.rst": [ - "fb276ded8a2b28ccf3d01a44c5daff0ff8e1e5ee", + "index.md": [ + "ad92b5a398257f95079edfa70744a19fa5edc3f7", [] ], - "license.rst": [ - "a341a31eb98e2091157bd6e1b1285c9d67bc07e3", + "init.md": [ + "4aaa099957f1154a88da93a888f346a5c20989bf", [] ], - "names.rst": [ - "0fe953e6a53d3486fbc3dd3352afa4e5ba8d3c09", + "license.md": [ + "aced3448aa95e246200627ede18e49d42df6679f", [] ], - "overview.rst": [ - "b35f66f2ddc083b626c29b29b5497bac9f50d87d", + "names.md": [ + "c1e9107ba0b79c32602e22d6c67568155a36c9f5", [] ], - "python-2.rst": [ - "7ec9e5112c38fdb7f01b25b4671b437f4c33308c", + "overview.md": [ + "5824f70875de55dfdc2ca516a0b79787dfc3598e", [] ], - "types.rst": [ - "fbb90a7e930d1761e502be708c712d9906160e23", + "types.md": [ + "5ab7146f6e9bb26dbf2f6106973aaf20396d38b2", [] ], - "why.rst": [ - "2c0ca4cd66e147bbbd98aae51f26d6fc330e517f", + "why.md": [ + "eeba9db5857f0c4733d34c7425a479d8263058cd", [] ] }, - "mypy.ini": [ - "685c02599f52259e92aa66922af8935ec3219092", - [] - ], "pyproject.toml": [ - "52c0e49ec2e4db3c934916c72c9810287739354e", - [] - ], - "setup.py": [ - "00e7b012ae72a30ede9e1254d32485b628bdbb9e", + "1c72fc26d6ab769df1cb17450c109dfc440a9d25", [] ], "src": { "attr": { "__init__.py": [ - "f95c96dd5795b9c958adfebbd36ffad99cf23cc9", + "9226258a2d58777f1d5536c5695bbf1b4a635991", [] ], "__init__.pyi": [ - "c0a21265036a6f6ceb35eeff504e5d1189db6400", + "37a208732acf774ed369811d51f1393798f22148", [] ], "_cmp.py": [ - "6cffa4dbabda22d3f2921af3d66456006a382b32", + "a4a35e08fc9d9b078a11edc3236d7e27027cd28e", [] ], "_cmp.pyi": [ - "e71aaff7a19b7a60ae49738bd81348e80af53a13", + "f3dcdc1a754146303b28009cbff9ec9bf960e450", [] ], "_compat.py": [ - "dc0cb02b6435bb4cb90f1d9645150d32286379a5", + "46b05ca453773da7f2972f023c4a4f447b44e824", [] ], "_config.py": [ - "fc9be29d0081225dfa01fb6dcd7188d665e4b101", + "9c245b1461abd5dc5143f69bc74c75ae50fabdc5", [] ], "_funcs.py": [ - "4c90085a4013bf906a726597f52b206d4c842b22", + "a888991d98fdac72abb6e5ce8ac6d620a8f0e54b", [] ], "_make.py": [ - "d46f8a3e7a42338af1f6e8763b8df0c1b64e3d89", + "10b4eca779621c819060c4564379fa2c098c36d5", [] ], "_next_gen.py": [ - "068253688caf890fa997fa81bfed38cbeffd4c45", + "1fb9f259b53b851336c3135f06cfa377ab3240d7", + [] + ], + "_typing_compat.pyi": [ + "ca7b71e906a28f88726bbd342fdfe636af0281e7", [] ], "_version_info.py": [ - "cdaeec37a1ad988cafb7e202967f48829c9d85ba", + "51a1312f9759f21063caea779a62882d7f7c86ae", [] ], "_version_info.pyi": [ @@ -471856,15 +472562,15 @@ [] ], "converters.py": [ - "1fb6c05d7bb8893fcdd8ed81f2a708cbf21d98f6", + "2bf4c902a66faeeda4cbae89d75f063df99c5039", [] ], "converters.pyi": [ - "0f58088a37be31f413c0adf04af32feff584b740", + "5abb49f6d5a8c3447d0f223a308e2278ad027416", [] ], "exceptions.py": [ - "b2f1edc32a941b3f05c708af43f5a1b284b72fc9", + "3b7abb8154108aa1d0ae52fa9ee8e489f05b5563", [] ], "exceptions.pyi": [ @@ -471872,11 +472578,11 @@ [] ], "filters.py": [ - "a1978a87755ba454296a789161d044d2d51d10e0", + "a1e40c98db853aa375ab0b24559e0559f91e6152", [] ], "filters.pyi": [ - "993866865eab7ede46b6421c6f31c1e79c02fd6a", + "8a02fa0fc0631dde0b4501c8d1c168b467c0d1a9", [] ], "py.typed": [ @@ -471884,41 +472590,41 @@ [] ], "setters.py": [ - "b1cbb5d83e744dc1a2a3012f6ebe7d4f6d72c740", + "12ed6750df35b96e2ccde24a9752dca22929188d", [] ], "setters.pyi": [ - "3f5603c2b0cb7cd4afc3fc7ed53bbaed073f679f", + "72f7ce4761c343860d8b230dd50dcdeba10b03fb", [] ], "validators.py": [ - "0b0c8342f2528678c1ab84b027abb12175f59fc7", + "34d6b761d37857e876a7d0fd1970a758f8f71981", [] ], "validators.pyi": [ - "5e00b8543397f8c9c353d7b7129db3dda13af469", + "d194a75abcacfa434f2445e66ea25975236dffcf", [] ] }, "attrs": { "__init__.py": [ - "a704b8b56bc0ff8562c5fe7e283e09154129b264", + "0c2481561a93a912503754396782e987fcdd9629", [] ], "__init__.pyi": [ - "7426fa5ddbf2068403835061cdc3a5095c2c6dc3", + "9372cfea16e89790cb0f515b1d9d48d8f1897151", [] ], "converters.py": [ - "edfa8d3c16ac8642773651778012a3cd57005d9b", + "7821f6c02cca81277d1ecc87b6bdafad886d8b70", [] ], "exceptions.py": [ - "bd9efed202ab1cdf57a9e99cb4e094ef6f38d0c0", + "3323f9d2112c54b203763d45b455bd5abbe020f6", [] ], "filters.py": [ - "52959005b088f0e5116c8b6acdbcc5937bbaacc8", + "3080f48398e5ed8d3428ca3efeb7500633b0cb0f", [] ], "py.typed": [ @@ -471926,11 +472632,11 @@ [] ], "setters.py": [ - "9b50770804e4187f0c935ef17bddf2d9a61120ff", + "f3d73bb793dd49c138950961f41943bb26c57fde", [] ], "validators.py": [ - "ab2c9b3024714d3b1caeb2f0773a0274dfc10f01", + "037e124f29f32d37c1642d159bf828de44f7c349", [] ] } @@ -471941,116 +472647,128 @@ [] ], "attr_import_star.py": [ - "eaec321bac43548dc7d4f5afd58a7b380b326453", + "bdc5c091b7f49c43debc8f9db044c2961ebf1496", [] ], "dataclass_transform_example.py": [ - "49e09061a8a2e57dd1da7b255cca3a39baf17181", + "c65df14026da81d36326424f8373dbd89f255ff5", [] ], "strategies.py": [ - "99f9f48536b9e67d71927dd6f4415938652b532f", + "783058f837f6cdc98fbdd38402a0f3a6b8cfb776", [] ], "test_3rd_party.py": [ - "8866d7f6ef2646f05d01110a1ede3c493d9379c6", + "b2ce06c293bfbddc8dd2f6ce4323515963079dd5", + [] + ], + "test_abc.py": [ + "a70b317a3cee1ef00f48b6ab6890075d7f0acdff", [] ], "test_annotations.py": [ - "a201ebf7fa6892ac0f569c643839ea9ede5220f2", + "d27d9e3743fe93949ca6b699a40ff25397a78d06", [] ], "test_cmp.py": [ - "ec2c6874899b73499a039cb9eb05ef261ea62c8b", + "07bfc5234ade5103d8f0e34ca26516043ade9ff1", [] ], "test_compat.py": [ - "464b492f0fac0f70e1fd797346ee4b2bd2612f72", + "c8015b596e2b07d0f891f0ef8390c964d96ce800", [] ], "test_config.py": [ - "bbf67564064ddab4ef3feb5a2c028bfafb12d64a", + "6c78fd295b5382ebe4b9c11d832217f92e2e77da", [] ], "test_converters.py": [ - "d0fc723eb1b8e52e501e02dd88b2777d9e182bcb", + "7607e555066c7e460bf6bf566230895f68e41a8a", [] ], "test_dunders.py": [ - "186762eb0da6eb0d9cdeb2839aa8e54fa42ca296", + "d0d289d84c9f15ec1e166a1f7167c0c8cdd019ae", [] ], "test_filters.py": [ - "d1ec24dc6c2c0880c50734e3f89e3f5846a87715", + "6d237fdc3d13d6133079d19cdc117365e23f5b50", [] ], "test_funcs.py": [ - "4490ed815ae3ed39d5312d3514c78e502de6d8c3", + "044aaab2c94a8ee110a466399f368bc403b98af0", [] ], "test_functional.py": [ - "9b6a27e2f4d4ef19049bc7dac21048454869a638", + "341ee50a82ae67c2da89af202ed097be6471d688", [] ], "test_hooks.py": [ - "92fc2dcaab5f9cce1ab77e48fa5b30e601177f78", + "9c37a98cdc0193c0cdcb3ab76531ac8be42cbb52", [] ], "test_import.py": [ - "423124319c9b133b1dbf22c09554bb75e6dc5605", + "9e90a5c11e685e7b9c14d5f84bb645c6a4f473cd", [] ], "test_init_subclass.py": [ - "863e794377d871c0059a67b1838a142a86ab2bd5", + "cff4e948bcbe8e7cc8e6ddf545d57c7f6ef74fee", [] ], "test_make.py": [ - "729d3a71f0692249769549c24f5469a6526d7b65", + "19f7a4cd412c9acdfcb85c8709a8ffbd78025819", [] ], "test_mypy.yml": [ - "ca17b0a662a917e62eeb29bd0a1c385fc4278029", + "0d0757233b4cfa1ee2fd135fc1a20e53175cba90", [] ], "test_next_gen.py": [ - "8395f9c0286553f9286598ebb816b09f75e11ef5", + "7d053d2143911f50f689825eab6a41743b4b7f96", + [] + ], + "test_packaging.py": [ + "046ae4c39dd06086268f401886cd5aa87d2e47ea", [] ], "test_pattern_matching.py": [ - "590804a8a7ac4911da982af185e2fb01f72b9adc", + "3855d6a379c24b3a977a210d400282d6e829c57f", [] ], "test_pyright.py": [ - "c30dcc5cb1613d05ecb739d9ab41b4902b3d0a50", + "800d6099fab0fe3f944b4006ab741beee96faa0a", [] ], "test_setattr.py": [ - "aaedde5746489b1faa705a35fe2de787525613ff", + "c7b90daee68b91b4728452983b1aa9efbc3e5d0f", [] ], "test_slots.py": [ - "baf9a40ddb88dad5efe280d4f1a3676f2d623793", + "26365ab0d2bcd8299d5d4a67c0ff787aa7344045", + [] + ], + "test_utils.py": [ + "92c04a1b503f45188a754ed84135840bd543a01d", [] ], "test_validators.py": [ - "d7c6de8bad793d8dc883f4bad53ae309b92b1753", + "4327f825188d582171c38a255ec592c52386e7e3", [] ], "test_version_info.py": [ - "41f75f47a6d871ba29be2fe29b787add687c2b40", + "5bd101bcce7d31c2769f3219d06027e3cf287c3a", [] ], "typing_example.py": [ - "a85c768c104abc9c38b48b61dd9a7f66fe81d647", + "2124912c8d58e5113b6b1d6aac0f689b7e3b4d9b", [] ], "utils.py": [ - "a2fefbd6068b4fdeeefe679ddf67a9e8fce24fac", + "9e678f05f17c62d8618274794ec1d517ee91dea7", [] ] }, "tox.ini": [ - "ddcbc4dbbcd995017e5fb1b19c5aa7ca9d611873", + "54724faaafbf6c4e399623d29920604de8e11793", [] ] }, @@ -472120,6 +472838,102 @@ [] ] }, + "exceptiongroup": { + "CHANGES.rst": [ + "ea8cbea8f7b95c8d831d2fa1696d69eab1920c35", + [] + ], + "LICENSE": [ + "50d4fa5e68439ce837f6eef437b299c0dd7c8594", + [] + ], + "PKG-INFO": [ + "2e8819b83b6cb7494c851cb850805ecb54167ddb", + [] + ], + "README.rst": [ + "d34937d576e47d9eb6a446b45a6e98bc2c239c38", + [] + ], + "pyproject.toml": [ + "aa47cdcec4c1d36374443ca0d396150469cd81e9", + [] + ], + "src": { + "exceptiongroup": { + "__init__.py": [ + "d8e36b2e65d11e7f3b2c540c1b292a39a6cc219d", + [] + ], + "_catch.py": [ + "0246568bd05013ed797e0514181aa43bdc59c63e", + [] + ], + "_exceptions.py": [ + "a4a7acea822d43ba91183cff0b44f31313171d1a", + [] + ], + "_formatting.py": [ + "e3835e41450763ffd00cc4fcb957545b24742080", + [] + ], + "_suppress.py": [ + "11467eeda9b317cbf5d378beea30e31a51d35d1c", + [] + ], + "_version.py": [ + "9e1bb0b65ea2faa3b6aa76fcad28138eba39d21f", + [] + ], + "py.typed": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ] + } + }, + "tests": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "apport_excepthook.py": [ + "1e4a8f30deeac30ae534247ce59dfa6a4ee6c5fa", + [] + ], + "check_types.py": [ + "f7a102d5d8ab278e6bbb45fd9572d6ea41a723b2", + [] + ], + "conftest.py": [ + "aeca72d3b5e6f178a10d7913fb593b45b92fd342", + [] + ], + "test_apport_monkeypatching.py": [ + "998554fde690d23fe93e3f879d14b816323a7d25", + [] + ], + "test_catch.py": [ + "1da749e23c6ed9b75f6f1e2c2fb27ac1f3048207", + [] + ], + "test_catch_py311.py": [ + "2f12ac32a28361a36d7e8c1312e3b0fccb6ce494", + [] + ], + "test_exceptions.py": [ + "f77511ab61e277226c225bee1fc4bae395d1f339", + [] + ], + "test_formatting.py": [ + "f6b9bc2c455e53793d5c67c9abae74679cb6a524", + [] + ], + "test_suppress.py": [ + "289bb33c0e67fb99b149a306c9e32db8455cbb52", + [] + ] + } + }, "h2": { ".codecov.yml": [ "db2472009c60acc72ca2f6f065bb4b674dc66746", @@ -475774,23 +476588,31 @@ }, "pluggy": { ".github": { + "FUNDING.yml": [ + "b4d3edff1f6a6d4ee43d56565cf5983ef519c1ba", + [] + ], "workflows": { "main.yml": [ - "e1022ca96dd2fcd2b3a39ebad2b29eddbd7d2c1a", + "d8c0b181a7c4ae76a2d9b76716de06c97cc99de0", [] ] } }, ".gitignore": [ - "4580536c7ade74858b0564b36facee8dcd497940", + "1a33c625bbe425c3ac51361842c5bf4f73dff9d6", [] ], ".pre-commit-config.yaml": [ - "d919ffeb2f6459828a44326e0c350086143f68b4", + "0532a6f7bf6b762e7129df62ef2b807645fc62b4", + [] + ], + ".readthedocs.yml": [ + "d751431ea38734958332bac725cdc6fab2dc082d", [] ], "CHANGELOG.rst": [ - "13a388c435a8c32a953dfff628e12d033910d8ec", + "a597dc370a6aed2a068e369c6af2dac1fd9f228c", [] ], "LICENSE": [ @@ -475802,11 +476624,19 @@ [] ], "README.rst": [ - "3496617e1e8796b256cdfbf16e0e68b5d4d22a77", + "7a58c1b9c8d77275c3f7f23c3eb1f6740e8c4db5", [] ], "RELEASING.rst": [ - "ee0d1331e0a46cb220c4aa86ad138b9da41ec264", + "3d6ba16c16b41fc7b9a7a3f1ddd2637de3448d9c", + [] + ], + "SECURITY.md": [ + "6d3c7348e89c8b60c59eff5f55a0b00a48b7c426", + [] + ], + "TIDELIFT.rst": [ + "93af996b296146e4f68927a620e15af3d91127ee", [] ], "changelog": { @@ -475833,7 +476663,7 @@ } }, "api_reference.rst": [ - "d9552d44858ae4a4728a4581ca1b828741495f8a", + "b14d725d94cb669cd20d55ecfda5811cdaac8306", [] ], "changelog.rst": [ @@ -475841,22 +476671,22 @@ [] ], "conf.py": [ - "f8e70c88bf3de92aff111c1590b0ed4092df7cf6", + "e5151c5a45ced2406444cfb6623f92c76c4f8690", [] ], "examples": { "eggsample": { "eggsample": { "__init__.py": [ - "4dc4b36dec3ac5f3b4663afc18d5a539c0e4272e", + "b2d9d8301facc8583dcd503be8f7f2004b137cac", [] ], "hookspecs.py": [ - "48866b2491287bafef248711c1d0da5262d72353", + "4bab42281de961f2813fc5b8fa4b9007073e8c28", [] ], "host.py": [ - "ac1d33b4539922b0ac862bad0676c911a33e8893", + "d18278792362fe71f3a4aedf7b66140c3508c4db", [] ], "lib.py": [ @@ -475865,7 +476695,7 @@ ] }, "setup.py": [ - "8b3facb3b6718ddda2d8f1d7e2c3efefb8628b13", + "89e88ceca07c13167ad4b344c9f9c55dd172e0f5", [] ] }, @@ -475875,36 +476705,74 @@ [] ], "setup.py": [ - "f81a8eb4038792e22038b2696ef7d7986628c20b", + "557aa5c1f1358dd0075259345b4ef9b58a0f171c", [] ] }, "toy-example.py": [ - "6d2086f9ba3bda66d7c41f79c16c6ab0309183a7", + "c7d361bc4dbea3ba37ead2bbb370570335c7bfc2", [] ] }, "index.rst": [ - "eab08fcbbd61613c66c9cae0e19b5f5b03dec1cc", + "b98c4956ba2e6063637541367fc4b818912218c3", + [] + ], + "requirements.txt": [ + "7d0b87a35ece581898deb2090b290763353a4ffb", + [] + ] + }, + "downstream": { + ".gitignore": [ + "0dc1814e36923955989de1d0561a6f23e54c6e37", + [] + ], + "README.md": [ + "ff420e7d9d941607de40da5faa1b226ad5a928ba", + [] + ], + "conda.sh": [ + "685d08d41b5416e4d3bf9329eca035d052f4b5d3", + [] + ], + "datasette.sh": [ + "7d3c5586b44c0e166b4970d05de4eb2462ab30c4", + [] + ], + "devpi.sh": [ + "7ef09c8da07e0c5c33c17759e7c0dbf5ad5a7e3a", + [] + ], + "hatch.sh": [ + "933e0f637b7c9c0a9aa8ac4f7183eec5b2a0cb3b", + [] + ], + "pytest.sh": [ + "5afc5612f0a4a644922c9433cefb043320804743", + [] + ], + "tox.sh": [ + "79e12dfa2b1329a607e3f2dabe49eb7764ecc3bf", [] ] }, "pyproject.toml": [ - "15eba2689830e830115e09e3dd97541d41cd40df", + "e286825c4b463f916ed9a3eb0e57b9d47f748ccb", [] ], "scripts": { "release.py": [ - "e09b8c77b1163d82b4169104dc5f608731559849", + "879d35dfd4b70236b8f8b20d9d29907ea1bf90fa", [] ], - "upload-coverage.sh": [ - "ad3dd482814c50aa71eed98f07ff8339f5e6078b", + "towncrier-draft-to-file.py": [ + "a47caa8fa8db634de9a22cf0d0b1716650cc84da", [] ] }, "setup.cfg": [ - "7040bcb83bec5842505d2d7bed42565bee471a1b", + "1e34d361203660b1bc21c92583834c0b5511644d", [] ], "setup.py": [ @@ -475914,721 +476782,93 @@ "src": { "pluggy": { "__init__.py": [ - "979028f759f2f5e246d75a66f40d4e7b32fff018", + "36ce16806219d4bdf9c628a70ce581b9b5208c9e", [] ], "_callers.py": [ - "7a16f3bdd40f4e5188598cc3094bb0cffaa4e12a", + "d01f925cca20f98c93e141f5954295c59b2fba03", [] ], "_hooks.py": [ - "1e5fbb75958104f53ebed10d2996964cd71f94a4", + "362d791823ee675fbd7092edba74e393372837e4", [] ], "_manager.py": [ - "65f4e5084276c9eb594fbc48d4086149828bae79", + "9998dd815b53f1f643cf4eee38af342013e5ab8b", [] ], "_result.py": [ - "4c1f7f1f3c01c4bff353b06278879110c44e73da", + "f9a081c4f6881cb70633479f8f8fdf21e2cb6d91", [] ], "_tracing.py": [ - "82c016271e1eab7e5c81c4658178b4b2eb07a775", + "cd238ad7e5495759cad0723e9611340271d5c3fd", + [] + ], + "_warnings.py": [ + "6356c770c7d1f26ef4f90a4781b1386209f8169c", + [] + ], + "py.typed": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", [] ] } }, "testing": { "benchmark.py": [ - "b0d4b9536a0107d7ddd1705e55b4ba002b46a44b", + "d13e50aa465b11a7d32fc71ba00dc2a92d009fd3", [] ], "conftest.py": [ - "1fd4ecd5bdf4c0f0b631eeb0a042e140d10edf01", + "8842bd7264422696222d8be8be91a135d60d3f13", [] ], "test_details.py": [ - "0ceb3b3eb13947dbfe1fa1a9684f58866775d59b", + "9b68a0814f8a3e406d38f02cd43da85b78365df8", [] ], "test_helpers.py": [ - "465858c499cb721d931f90579d5a812cd9893951", + "4fe26a57bc50c41eb0e8f7d8e05cea21517088a5", [] ], "test_hookcaller.py": [ - "9eeaef8666652414fbac465c554d63449fd059da", + "3db76de2562fe8ad3095171601236fb891a9b0df", [] ], "test_invocations.py": [ - "323b9b21e8b6d05ab511e4885e2673a86e717d7d", + "833ec2b79cec81500da038b1e51aedd21342f7c7", [] ], "test_multicall.py": [ - "8ffb452f694ba9f2f2d1bb18975783f11464aeea", + "7d8d8f2881a421b868ab06f28752a742a43f1575", [] ], "test_pluginmanager.py": [ - "304a007a5894af39e7184802fd04ed94ecd4fcd5", + "c4ce08f3bc39a47389255217885e3ec2cf011d01", [] ], "test_tracer.py": [ - "992ec679149b5d26f68a2734cedbc6debdd9352e", - [] - ] - }, - "tox.ini": [ - "97b3eb77920920e8ff3af30b87310546c06e566f", - [] - ] - }, - "py": { - ".flake8": [ - "f9c71a7fbc4d04390b183812d0ebfbe9762c90ba", - [] - ], - ".gitattributes": [ - "1246879c4daac82abe554090e60e11fc2252f33b", - [] - ], - ".github": { - "workflows": { - "main.yml": [ - "564aa0c5315d34fa03e031bcb8614a46a7aa0e90", - [] - ] - } - }, - ".gitignore": [ - "fa936f1596d0191e0e9a8f83638368157b77792f", - [] - ], - "AUTHORS": [ - "9c5dda9ceb0ea03a6972d863f0dd05f7b09a3e3e", - [] - ], - "CHANGELOG.rst": [ - "47c6fdb7a1750cf5a690fa1185fb368be94a4fa1", - [] - ], - "LICENSE": [ - "31ecdfb1dbc54fb41999867884dad52a07eac2ca", - [] - ], - "MANIFEST.in": [ - "6d255b1a9e5b63504cdcd5ca4ba47f1f18681e57", - [] - ], - "README.rst": [ - "80800b2b7ae7525389131aa72419ec9b8e9cc431", - [] - ], - "RELEASING.rst": [ - "fb588e3ab742d06a9f9e0a49fbd5268c183fe41e", - [] - ], - "bench": { - "localpath.py": [ - "aad44f2e66913be04d85a8a44317e59e0517177e", - [] - ] - }, - "codecov.yml": [ - "a0a308588e20127cc7ad0ab58356b6b677391be1", - [] - ], - "conftest.py": [ - "5bff3fe02245ce0d01a8f0011cd0c5e1f411f01f", - [] - ], - "doc": { - "Makefile": [ - "0a0e89e01fe61e51f0c758b09267bc2052e41315", - [] - ], - "_templates": { - "layout.html": [ - "683863aa4603cea38afb9635778ead728220dd33", - [] - ] - }, - "announce": { - "release-0.9.0.txt": [ - "0710931354312054004ca4ff293de49f47c37b61", - [] - ], - "release-0.9.2.txt": [ - "8340dc44557b228e3cf6f826c2880f965096a03b", - [] - ], - "release-1.0.0.txt": [ - "aef25ec239b54ecd8050b4958638af27cea793c2", - [] - ], - "release-1.0.1.txt": [ - "0c9f8760bdbad4b5d0b1db69d664aee3bcbfe221", - [] - ], - "release-1.0.2.txt": [ - "235461953530439ba661a776bb6edc47778a8a66", - [] - ], - "release-1.1.0.txt": [ - "0441c3215eb11667d312bbdc2e7ba79e89119981", - [] - ], - "release-1.1.1.txt": [ - "83e6a1fd8d95853b6301c0f1c4560e3769fc5866", - [] - ], - "release-1.2.0.txt": [ - "4f6a561447633a751a63166c5d14960473ee0459", - [] - ], - "release-1.2.1.txt": [ - "5bf8ba22dc6990be77df56ab4c5ea8efd1b8bab4", - [] - ], - "release-1.3.0.txt": [ - "cf97db0367a8f8071d6738b3372aac06a2998b79", - [] - ], - "release-1.3.1.txt": [ - "471de408a100ee51eb5e63246d424f3b3d7749b8", - [] - ], - "release-1.3.2.txt": [ - "599dfbed755f7ddecfc2a4ae2dc376217cf31030", - [] - ], - "release-1.3.3.txt": [ - "c62cb85905379970a5755f54e8e32c4539a25b19", - [] - ], - "release-1.3.4.txt": [ - "c156c8bdb33c06969f40c1e8bcbe2e90dc956c44", - [] - ], - "release-1.4.0.txt": [ - "1c9fa75604a5cf889b7ea8b61078f9fcd027bc4a", - [] - ], - "release-1.4.1.txt": [ - "6ed72aa4183e83240475c1abe5e05f8f4e5c6540", - [] - ], - "releases.txt": [ - "309c29bac5de3a15546abc5562573549d9a9b020", - [] - ] - }, - "changelog.txt": [ - "0c9d0928e7ae3a6d158e6fc40c940856a9a92da6", - [] - ], - "code.txt": [ - "bdd8691da03e2e3a2f768d8a8877c7d4e410f382", - [] - ], - "conf.py": [ - "de4cbf8a46f1052f54713e2b51233351e13cd13a", - [] - ], - "download.html": [ - "5f4c466402d14ccaf6302aa48e3933a726c54a09", - [] - ], - "example": { - "genhtml.py": [ - "7a6d4934970872fc9f7da7e70569d14a2033279a", - [] - ], - "genhtmlcss.py": [ - "facca77b78b5139f2b6f3918ad75233f1faf9585", - [] - ], - "genxml.py": [ - "444a4ca52cc37cb8d379d5f738602c70728c8908", - [] - ] - }, - "faq.txt": [ - "6d374e1db9b3bc689bf85bcebcdd301934dab259", - [] - ], - "img": { - "pylib.png": [ - "2e10d438866861ebaf917cd412e01c9d0c72ae9a", - [] - ] - }, - "index.txt": [ - "c700b17e9877bef5b20365b5b42ef71862611cc1", - [] - ], - "install.txt": [ - "93c79e3b2d8f39261dc0602d85812fb28e5bb136", - [] - ], - "io.txt": [ - "c11308a6d2859dff898503ff5ab674b058e8ad11", - [] - ], - "links.inc": [ - "b61d01c696a1ea370a3d31b5713416356c359a41", - [] - ], - "log.txt": [ - "ca60fcac250542577507d6c7018e6fabbc8be117", - [] - ], - "misc.txt": [ - "4b453482757a106a6f3fa440b7d40f024ce46a39", - [] - ], - "path.txt": [ - "8f506d49232704ee6d67897a02d8a45efaa93221", - [] - ], - "style.css": [ - "95e3ef07b23b554ce4d89afe7a6e7dff47eb3e8e", - [] - ], - "xml.txt": [ - "1022de6e9126f81609c92eca97c58f04d62711e6", - [] - ] - }, - "py": { - "__init__.py": [ - "b892ce1a2a6b5a743a78fb95766c2f9868b4d70b", - [] - ], - "__init__.pyi": [ - "96859e310f4c8749b3e6bd250b338857b1047945", - [] - ], - "__metainfo.py": [ - "12581eb7afbc231e02476c125ccb9e289e6f3024", - [] - ], - "_builtin.py": [ - "ddc89fc7be6e4b9723e9dbf0b4ffbcb64320d610", - [] - ], - "_code": { - "__init__.py": [ - "f15acf85132113f090b1a53cdd0bac39af924d4d", - [] - ], - "_assertionnew.py": [ - "d03f29d870814283d3f2d537d91b8a527821ee1f", - [] - ], - "_assertionold.py": [ - "1bb70a875d059d664c6518701963864afad86593", - [] - ], - "_py2traceback.py": [ - "d65e27cb73077bbd33bc0fad0d20a89e443bef9e", - [] - ], - "assertion.py": [ - "ff1643799c9e015a8e351b89958ae3eb8111d668", - [] - ], - "code.py": [ - "dad796283fe64c89f5f99f2bc72adc4a7657d238", - [] - ], - "source.py": [ - "7fc7b23a96c32e603f1e678d5dad272e84a0e27d", - [] - ] - }, - "_error.py": [ - "a6375de9fa29b148e8883725f1fc385228b5c596", - [] - ], - "_io": { - "__init__.py": [ - "835f01f3ab9dcb656dce1e580f0d98d7b8abfe3a", - [] - ], - "capture.py": [ - "cacf2fa71a104bd3a4c25582bded149faa0c1a9f", - [] - ], - "saferepr.py": [ - "8518290efddecdc8524c642abbd5aba76dada44c", - [] - ], - "terminalwriter.py": [ - "442ca2395e0a15e8cddf2da9565d02b2c807faaf", - [] - ] - }, - "_log": { - "__init__.py": [ - "fad62e960d4fc0d5faf479467aaa0bbf57008a52", - [] - ], - "log.py": [ - "56969bcb58c3322248efc7de7a04fef96074fe65", - [] - ], - "warning.py": [ - "6ef20d98a2dc0e7be240c593e0303c501ecb7835", - [] - ] - }, - "_path": { - "__init__.py": [ - "51f3246f8070ff46342d3d9985fa384ac4e1d9f8", - [] - ], - "cacheutil.py": [ - "99225047502c6a7119caf136af06228235df9fd1", - [] - ], - "common.py": [ - "2364e5fef504a6b20000efc9b2d47bff1defef27", - [] - ], - "local.py": [ - "1385a0398742b72fead43bbc64faae4d315f31d4", - [] - ], - "svnurl.py": [ - "6589a71d09e33b946c0d7c90c5945cdfc9c8a7a0", - [] - ], - "svnwc.py": [ - "b5b9d8d544a25253dab87a4f9d1bc3752f38ba06", - [] - ] - }, - "_process": { - "__init__.py": [ - "86c714ad1aed3b1b511d90c1e1ce6cb22d662ec3", - [] - ], - "cmdexec.py": [ - "f83a2494029687038343de2900cc2ac7e19c8747", - [] - ], - "forkedfunc.py": [ - "1c285306884bf2f0310d8b4f94a4ae2f772fe04f", - [] - ], - "killproc.py": [ - "18e8310b5f6caaf884473ba11c4c4d3b6f78c7d3", - [] - ] - }, - "_std.py": [ - "66adb7b0239e290e9a32b5d7447dbf48262b7cd8", - [] - ], - "_vendored_packages": { - "__init__.py": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "apipkg": { - "__init__.py": [ - "350d8c4b07a1334202b72b897ba2bd466eb78534", - [] - ], - "version.py": [ - "c5b4e0e79fa23b21a985639c250793385b395515", - [] - ] - }, - "apipkg-2.0.0.dist-info": { - "INSTALLER": [ - "a1b589e38a32041e49332e5e81c2d363dc418d68", - [] - ], - "LICENSE": [ - "ff33b8f7ca0b1c05bb0bdc546aa760c8e78757be", - [] - ], - "METADATA": [ - "7eea770a02bdb9b84a8fed46f92652eb61e71277", - [] - ], - "RECORD": [ - "357b8b9c729f86703d61441ccc974931b0844e4e", - [] - ], - "REQUESTED": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "WHEEL": [ - "b733a60d379c60aef4921d7e42a113fdc28300dc", - [] - ], - "top_level.txt": [ - "e2221c8f9e9639243d2bce2e9230b49d413b4521", - [] - ] - }, - "iniconfig": { - "__init__.py": [ - "6ad9eaf868b073bb33aac5ee9814e4efe2e19da1", - [] - ], - "__init__.pyi": [ - "b6284bec3f65439b5020702cf09fac412d1e4917", - [] - ], - "py.typed": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ] - }, - "iniconfig-1.1.1.dist-info": { - "INSTALLER": [ - "a1b589e38a32041e49332e5e81c2d363dc418d68", - [] - ], - "LICENSE": [ - "31ecdfb1dbc54fb41999867884dad52a07eac2ca", - [] - ], - "METADATA": [ - "c078a7532fbbb182a4f327c8aa58de8bbd04fa03", - [] - ], - "RECORD": [ - "168233330b65d1fc209b220f4b5e47421173b396", - [] - ], - "REQUESTED": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "WHEEL": [ - "6d38aa0601b31c7f4c47ff3016173426df4e1d53", - [] - ], - "top_level.txt": [ - "9dda53692d2f44a97392cae823f8163d55ad4549", - [] - ] - } - }, - "_xmlgen.py": [ - "1c83545884390b724079d4c0532963a2a054f9be", - [] - ], - "error.pyi": [ - "034eba609f19b52f0cedf3d061b4b0ad96168a33", - [] - ], - "iniconfig.pyi": [ - "b6284bec3f65439b5020702cf09fac412d1e4917", - [] - ], - "io.pyi": [ - "d377e2405d55b77ef406ddedcf0553b03389326c", - [] - ], - "path.pyi": [ - "1ddab9601ea43988162102407172f519c8cf1879", - [] - ], - "py.typed": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "5e538369446abc7f350f44cb4dad5bbbf335a4e3", [] ], - "test.py": [ - "aa5beb1789f057604d8f13433951ea092848c1f4", - [] - ], - "xml.pyi": [ - "9c44480a5f3df15abc27e21c9515af4d2b939165", - [] - ] - }, - "pyproject.toml": [ - "e386ea0b2718340978f4200de3a2da10140f870a", - [] - ], - "setup.cfg": [ - "5f25c2febfd94b62b56a225fa64aea342b93937f", - [] - ], - "setup.py": [ - "5948ef0047a55b43b752f553e424a33fe99aaf41", - [] - ], - "tasks": { - "__init__.py": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "vendoring.py": [ - "3c7d6015cfa75e6e02b24700420a0e38db9eb2c5", + "test_warnings.py": [ + "4f5454be24f55be4ba9c19fecb8c7e8bc5c5829f", [] ] }, - "testing": { - "code": { - "test_assertion.py": [ - "e2a7f90399845009e567e98da5fc698f814aa0d8", - [] - ], - "test_code.py": [ - "28ec628b00d43615ad6bd9f81c2fc9c203f0afb2", - [] - ], - "test_excinfo.py": [ - "c148ab8cfbd0b0e0358cae8c42558ac148c3edcf", - [] - ], - "test_source.py": [ - "ca9a42275cff29a7f370170e7107b25a224b8953", - [] - ] - }, - "conftest.py": [ - "0f956b3dd253d8b003fd1fbcf923a6826b8f234f", - [] - ], - "io_": { - "__init__.py": [ - "792d6005489ebee62cde02066f19c5521e620451", - [] - ], - "test_capture.py": [ - "b5fedd0abc629dc8060a76aef7b4bf9f6489ec1f", - [] - ], - "test_saferepr.py": [ - "97be1416fec6b467822f928129a7a2e26614d1c1", - [] - ], - "test_terminalwriter.py": [ - "44b4f1ddeecaa4239eda4dd435cd7c1d4eb11c93", - [] - ], - "test_terminalwriter_linewidth.py": [ - "e6d84fbf7a18be7271c2dcd8b0a48fcb05f1e300", - [] - ] - }, - "log": { - "__init__.py": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", - [] - ], - "test_log.py": [ - "5c706d9b6ad34dc7931c4c2eceb3b82cb395db9f", - [] - ], - "test_warning.py": [ - "36efec913a3bde0aea39efbef294a8b4e8045dd4", - [] - ] - }, - "path": { - "common.py": [ - "d69a1c39d099034638a37e36bf6262900c10ead8", - [] - ], - "conftest.py": [ - "84fb5c82694067fd162a58f2a06d44ffb63a6cd9", - [] - ], - "repotest.dump": [ - "c7819cad7a531a772fb5e32ba1fa9256a43314ac", - [] - ], - "svntestbase.py": [ - "8d94a9ca649cd3139c04e4adaecd26550436f9a2", - [] - ], - "test_cacheutil.py": [ - "c9fc07463a726a3138d2ebd033b50e14dc0cea32", - [] - ], - "test_local.py": [ - "1b9a7923f6539a80e9ec21d7e90e256eff96642d", - [] - ], - "test_svnauth.py": [ - "654f033224fbd43fe1d1602dfcab19d311201180", - [] - ], - "test_svnurl.py": [ - "15fbea5047da4f77fda9fb14e34d09eb7a6fab25", - [] - ], - "test_svnwc.py": [ - "c643d9983fb4db6ec3b0b85dd873268274bb24be", - [] - ] - }, - "process": { - "__init__.py": [ - "792d6005489ebee62cde02066f19c5521e620451", - [] - ], - "test_cmdexec.py": [ - "98463d906d151b0b8d375f5bbcdb2c0278335108", - [] - ], - "test_forkedfunc.py": [ - "ae0d9ab7e6d7494850b8117af192f0e8445311ed", - [] - ], - "test_killproc.py": [ - "b0d6e2f51531e0b6aa03b17687451a935bc1244f", - [] - ] - }, - "root": { - "__init__.py": [ - "792d6005489ebee62cde02066f19c5521e620451", - [] - ], - "test_builtin.py": [ - "287c60d552ae7fbf62b61d98b3c6389e9f330b59", - [] - ], - "test_error.py": [ - "7bfbef3bd4cdff6aef92c741e86b7b23fadae709", - [] - ], - "test_py_imports.py": [ - "31fe6ead810ce0cc4f93695ad6e716f8ea1daeb7", - [] - ], - "test_std.py": [ - "143556a0557f7d8dd11aebd8cbf4f5a675c168c2", - [] - ], - "test_xmlgen.py": [ - "fc0e82665f7ee9bfd894c90687b80c97dd6d72a5", - [] - ] - } - }, "tox.ini": [ - "f3203507fd9e366590373870b068148a1f9c6438", + "de464a078f10034d22300a32fd66ca9ebc1d7d73", [] ] }, "pytest": { - ".gitattributes": [ - "ec1c66dc1e91540ee991343be9aba518a991b5bf", + ".git-blame-ignore-revs": [ + "bce64a374beeee32b447aa7d33d16a78e3a6da3d", [] ], - ".gitblameignore": [ - "0cb298b024d3a5a611ce581460e788a6a63f6470", + ".gitattributes": [ + "ec1c66dc1e91540ee991343be9aba518a991b5bf", [] ], ".github": { @@ -476659,7 +476899,7 @@ [] ], "dependabot.yml": [ - "507789bf5a4cd5902358cfb5b4870f7c68382f76", + "294b13743e2d4eb3cf3596485a1801336cb0e192", [] ], "labels.toml": [ @@ -476667,34 +476907,46 @@ [] ], "workflows": { - "main.yml": [ - "42759ce853faf970fe0b5343e4f314a48c594f0e", + "backport.yml": [ + "38ce7260278cee33d56c63b7e457c548fc7433cb", + [] + ], + "deploy.yml": [ + "20a72270fde2ba4a5037cf35df9e882ec93d1bd0", [] ], "prepare-release-pr.yml": [ - "429834b3f2149030a329ba551cbed649d8ced08b", + "1bb23fab844706c9f1d92b90b521a477ec09452e", + [] + ], + "stale.yml": [ + "82f9a1f2579111951597c6b9a8c2b191f2addfc4", + [] + ], + "test.yml": [ + "09d37aaa2c868947f741d9bc97ac638c1379e3f9", [] ], "update-plugin-list.yml": [ - "193469072ffaab3aee483a93a1e3aa218bb302fc", + "6943e2076083c650cb83eefca0d8781af72a98e6", [] ] } }, ".gitignore": [ - "935da3b9a2effa9e582618ab2d2e59baa694b0da", + "9fccf93f7c3f32e37d4c687e49286a3dee2ba567", [] ], ".pre-commit-config.yaml": [ - "20cede3b7bbd54befe0edc33a26a29f4761be9da", + "a80edd28cdc751e8a8882588e0dfda0d1fe9a147", [] ], ".readthedocs.yml": [ - "bc44d38b4c7d73524c818efebab929611e46e5c9", + "266d4e07aead5fafdcdbddf96a748e118e18b8da", [] ], "AUTHORS": [ - "9413f9c2e749a8461efdf25d1f98466aa260c8db", + "54ed85fc73212650ddf6df77c4e5ff991a0c2664", [] ], "CHANGELOG.rst": [ @@ -476710,7 +476962,7 @@ [] ], "CONTRIBUTING.rst": [ - "24bca723c8bd340acc169ee9db7e3afc21a7c81e", + "d7da59c812dc87c7a7d5aad7fa0ab7bd0a4e706c", [] ], "LICENSE": [ @@ -476722,24 +476974,24 @@ [] ], "README.rst": [ - "14733765173f962a7f5274111df7ccc4581f8454", + "a81e082cdd7d65b9f6d1f1c36ac5d27bfe4fb032", [] ], "RELEASING.rst": [ - "25ce90d0f655addf46421c1423deb6f00b756720", + "08004a84c00a8a5faccf48d2f0a23ad90ec450c0", [] ], "TIDELIFT.rst": [ - "2fe25841c3a44a62c2a7ce835d5ebfbeaf3074b2", + "1ba246bd86846f71a65764f0c60d33f60798efd0", [] ], "bench": { "bench.py": [ - "c40fc8636c0c6570fab65f027e70aa0e295acdb5", + "437d3259d831251b76da053138bfc5014adcf2f2", [] ], "bench_argcomplete.py": [ - "335733df72b4314240436135a0cd893944488ec7", + "459a12f93141bd51bf939d5c167e0ea3243e443e", [] ], "empty.py": [ @@ -476751,11 +477003,11 @@ [] ], "skip.py": [ - "f0c9d1ddbeffbc421123816d4ebee4605a41eeb7", + "fd5c292d92c1b8f33bf9e01012f9bc078a6d4050", [] ], "unit_test.py": [ - "ad52069dbfdd2e65f42dfbc8e196351157df2c31", + "d3db111e1ae51e9cef150f9da268513f52dacdfc", [] ], "xunit.py": [ @@ -476765,7 +477017,7 @@ }, "changelog": { "README.rst": [ - "6d026f57ef3ed0cf3a5b51b4642aa58f40930b55", + "88956ef28d8b44d1537961ecac0e1065d28ea3a0", [] ], "_template.rst": [ @@ -476785,7 +477037,7 @@ ], "_templates": { "globaltoc.html": [ - "7c595e7ebf2a17556444f75eb9866361a740262d", + "09d970b64ed1fab682184957ed7edf9073ec43e3", [] ], "layout.html": [ @@ -476805,21 +477057,21 @@ [] ], "slim_searchbox.html": [ - "e98ad4ed905d3e5090988dafd5f1d036a2659fb2", + "f088ff8d31286776ed7ec90541b0916ce7501bbf", [] ] }, "adopt.rst": [ - "13d82bf011673d9f3e3028c91c48d906842c982f", + "b95a117debb8ff1d3255813f3f9ea9c0895d3948", [] ], "announce": { "index.rst": [ - "9505b0b9e46ebb5d766cbfc3d76d2652c9cf87c0", + "8a33f7fb57d1485f3e38dbc75bd161abcac1b5d0", [] ], "release-2.0.0.rst": [ - "ecb1a1db9889312788c64801cabc43b06f92c881", + "c2a9f6da4d59f5bcb2a67e73385c5cc7a8e3a4bf", [] ], "release-2.0.1.rst": [ @@ -476859,7 +477111,7 @@ [] ], "release-2.2.2.rst": [ - "22ef0bc7a166556eed69d85947da4aa68f927450", + "510b35ee1d029f99bd151725181a9ff73ef20114", [] ], "release-2.2.4.rst": [ @@ -476891,7 +477143,7 @@ [] ], "release-2.4.0.rst": [ - "138cc89576c214eb2f8105e6956824b46428035f", + "9b864329674b342643db6d2a057cdbcbcc513321", [] ], "release-2.4.1.rst": [ @@ -476903,7 +477155,7 @@ [] ], "release-2.5.0.rst": [ - "c6cdcdd8a830a3d6e9d5f85844d4f8f01019314c", + "fe64f1b8668afd8e8b61fe6154ed88943825b3f4", [] ], "release-2.5.1.rst": [ @@ -476915,7 +477167,7 @@ [] ], "release-2.6.0.rst": [ - "56fbd6cc1e4d9f4dfb6cfb4e30b6159a62a4231a", + "c00df585738b65380ce354d02998b1530cefcb20", [] ], "release-2.6.1.rst": [ @@ -476931,7 +477183,7 @@ [] ], "release-2.7.0.rst": [ - "2840178a07f7785c8e249e96440edcd00107b533", + "83cddb34157b71a1594834b0cd741425efd4a3ce", [] ], "release-2.7.1.rst": [ @@ -477398,25 +477650,125 @@ "5accfbad0d4b3fc091eba3665a9c2ee5e1fb88dd", [] ], + "release-7.1.0.rst": [ + "3361e1c8a328c86c3e0e3aaa77e4f2c1977ac0b2", + [] + ], + "release-7.1.1.rst": [ + "d271c4557a2182729d96cb9176d11f3dda9f5a8a", + [] + ], + "release-7.1.2.rst": [ + "ba33cdc694b5285b88699d6871af5e1d46221421", + [] + ], + "release-7.1.3.rst": [ + "4cb1b271704eff120f543ce44c9d2143bb55d62a", + [] + ], + "release-7.2.0.rst": [ + "eca84aeb669a9fa2a5b3ff7f7195c9633177fe67", + [] + ], + "release-7.2.1.rst": [ + "80ac7aff07f55234f0e33c80931734f9f43c5614", + [] + ], + "release-7.2.2.rst": [ + "b34a6ff5c1e91ec010bf602e8b7d9b75cbbf27ad", + [] + ], + "release-7.3.0.rst": [ + "33258dabadeb20dba9d72a47046e764a04bca486", + [] + ], + "release-7.3.1.rst": [ + "e920fa8af532e378013a0fa32fdbc1b6ce68bd1c", + [] + ], + "release-7.3.2.rst": [ + "b3b112f0d8e56f3198e41c0cf64734758d6cf63a", + [] + ], + "release-7.4.0.rst": [ + "5a0d18267d33899069e6f6609309aad1322d9dd2", + [] + ], + "release-7.4.1.rst": [ + "efadcf919e8a838c1b663cbe359147cae55b5f5e", + [] + ], + "release-7.4.2.rst": [ + "22191e7b4f95c9ad632f3e7da67e55864c3f28a8", + [] + ], + "release-7.4.3.rst": [ + "0f319c1e7f0d2fd52224237f63b1f2604f0aac43", + [] + ], + "release-7.4.4.rst": [ + "c9633678d2e2e10b7c151aea4c00dcc1c6e727cd", + [] + ], + "release-8.0.0.rst": [ + "00f54fd822504bdb531ef729a8883d28cff23d06", + [] + ], + "release-8.0.0rc1.rst": [ + "547c8cbc53baf0c88ef08a541614ef0d5884e20d", + [] + ], + "release-8.0.0rc2.rst": [ + "1a6444c5214a5ecfe2a656d3c01c4155258134ad", + [] + ], + "release-8.0.1.rst": [ + "7d828e55bd98e07ce8d01b04e16a7c40355487ca", + [] + ], + "release-8.0.2.rst": [ + "c42159c57cff7fcbc38d25b875bf22684f6c6d1c", + [] + ], + "release-8.1.0.rst": [ + "62cafdd78bb3649922213ef96607e98a5a1a4d03", + [] + ], + "release-8.1.1.rst": [ + "89b617b487dcf724d2b81f312b4f00c8bda376d7", + [] + ], + "release-8.1.2.rst": [ + "19e41e0f7c54b8fc2f7cbc09cae527b549ad705b", + [] + ], + "release-8.2.0.rst": [ + "2a63c8d87228689e0eb2909caca52ed4cc14f125", + [] + ], + "release-8.2.1.rst": [ + "4452edec110e5d62849cd2de0c0da70178c91d45", + [] + ], "sprint2016.rst": [ - "8e706589876710d40e76d76a7b62a26afb8a57cc", + "8d47a205c7168983ec86b8095a935f362aeebb67", [] ] }, "backwards-compatibility.rst": [ - "3a0ff126164619ee38339c14403b3cba68666207", + "e04e64a76f9a6d5f4f024390f3d7b2a2b8ea55ae", [] ], "builtin.rst": [ - "c7e7863b2188c865bc7b26e0831f949123533e8e", + "458253fabbb20ca0eb993e7f5b7df36c100c12df", [] ], "changelog.rst": [ - "1acdad366da6cefa643e951c48fe69f18e186d62", + "f69b9782bbc28827a6997eec246839b974cbf961", [] ], "conf.py": [ - "b316163532ac027473c03c8f78ec0226a1bddca9", + "32ecaa17435e766fd862945d1a56eb50d98586dd", [] ], "conftest.py": [ @@ -477428,7 +477780,7 @@ [] ], "contents.rst": [ - "049d44ba9d8a32489d73c363a83edb4363921e1d", + "181207203b2201c2371ed40649ec6f860aa5f81c", [] ], "contributing.rst": [ @@ -477436,7 +477788,7 @@ [] ], "deprecations.rst": [ - "0f19744adec31ddf366fe60ebc4389c93a9b2e94", + "a65ea3316632073202a6e82ca158a3a1c6259ee2", [] ], "development_guide.rst": [ @@ -477446,12 +477798,12 @@ "example": { "assertion": { "failure_demo.py": [ - "abb9bce5097a898387c7cc105a1b780e80f65db0", + "f7a9c279426865fac4c6861aa7cffb59ab3bb05f", [] ], "global_testmodule_config": { "conftest.py": [ - "7cdf18cdbc1316a812d6fe21f3b5d05527377c98", + "4aa7ec23bd1c98f2dca82361785b835ce49fd243", [] ], "test_hello_world.py": [ @@ -477460,7 +477812,7 @@ ] }, "test_failures.py": [ - "350518b43c76af35adc9fd56a403a7dbfd49671d", + "19d862f60b782208471498f3cd1f7a3d4101d5ae", [] ], "test_setup_flow_example.py": [ @@ -477469,11 +477821,43 @@ ] }, "attic.rst": [ - "2ea870062043591c0910cc50000016e76e1cbdbf", + "2b1f2766dce789ce59f31abd1c47b8ce5a32e688", [] ], "conftest.py": [ - "f905738c4f6c7ae5ef6d89b19feb1835d749e005", + "66e70f14dd713766a8f0476bb38f2f6520d40b19", + [] + ], + "customdirectory": { + "conftest.py": [ + "b2f68dba41a20e84038e1e6dd1b2b61cac18a500", + [] + ], + "pytest.ini": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "tests": { + "manifest.json": [ + "6ab6d0a522269fbe85a7a0fb9450a11b17ebc5ab", + [] + ], + "test_first.py": [ + "0a78de59945d3ab99f3659d9d1a554a973bb9d78", + [] + ], + "test_second.py": [ + "eed724a7d969fe6e95874d2b86d85e22419b2285", + [] + ], + "test_third.py": [ + "61cf59dc16c2a51e85e0c48c2bf570de4fdb49b3", + [] + ] + } + }, + "customdirectory.rst": [ + "1e4d7e370de299b6af6019d7d8473f2deafa04fe", [] ], "fixtures": { @@ -477514,7 +477898,7 @@ [] ], "test_fixtures_order_dependencies.py": [ - "b3512c2a64dbd4f79d446f4cc6c664acc20d7e4e", + "e76e3f93c622f3f8085bac318efa485b0acb2eb7", [] ], "test_fixtures_order_dependencies.svg": [ @@ -477547,15 +477931,15 @@ ] }, "index.rst": [ - "71e855534ff947dca1d86096e8786417f8bcb4c9", + "840819002d4c582b352bf9f9bff763c2d7e2eb1e", [] ], "markers.rst": [ - "3226c0871e0932568ffbc63c9a88ef29dbd3285b", + "c04d2a078ddbc375e5423f9277bb7b693257f656", [] ], "multipython.py": [ - "9005d31adddcd912ea0e1187c9d4234c8b97c04c", + "861ae9e528da3fe632825a28671bdf2021ac162f", [] ], "nonpython": { @@ -477564,7 +477948,7 @@ [] ], "conftest.py": [ - "bc39a1f6b204888e14e8ec66815f99793202ded8", + "e969e3e25187c80202bd52af5b78b258a8d7423e", [] ], "test_simple.yaml": [ @@ -477573,11 +477957,11 @@ ] }, "nonpython.rst": [ - "f79f15b4f79d3c0e250761ab3c082456919ddd73", + "aa463e2416bfd10d7f0f196b172477100ae9758f", [] ], "parametrize.rst": [ - "66d72f3cc0d430b2b575d72fdcb122ad12685099", + "03f6852e5c0f5f65ac6028bb81b75878b4cf37f5", [] ], "pythoncollection.py": [ @@ -477585,15 +477969,15 @@ [] ], "pythoncollection.rst": [ - "b9c2386ac51a63d64bf3d0143967dd5107696fe7", + "aa9d05d72275338db96d419eca6030cdde84d22f", [] ], "reportingdemo.rst": [ - "cab93143615592bc2bec86b363517c135e249da0", + "2c34cc2b00d3832e9e0a542f557ba7366af8fd18", [] ], "simple.rst": [ - "a70f34049928a22897801f400eab29506c0667d7", + "7064f61f0e23b5364e874f1056955190917dd200", [] ], "special.rst": [ @@ -477601,25 +477985,25 @@ [] ], "xfail_demo.py": [ - "01e6da1ad2ed20b633508c668df5805251de0f60", + "1040c89298d07b0fd2011f2e772ad8944ee70eca", [] ] }, "explanation": { "anatomy.rst": [ - "e86dd74251e841eaadc5f2515c65aa079b5044e4", + "93d3400dae2b3ee1cfc41a1bebb66286414273d6", [] ], "fixtures.rst": [ - "194e576493e5ae3f9bb49700365c0ffb076d9e7a", + "0bb3bf49fb09765a37c22fe17cc3c1c539343923", [] ], "flaky.rst": [ - "50121c7a761bde372b32bfe987283983fb6854d0", + "41cbe8479895b222c4d350cc571575d03b938e92", [] ], "goodpractices.rst": [ - "32a14991ae2a860fb26c0a7ae085b403f2433b0b", + "1390ba4e8fedb9bca0b1d899d8aa607db152df47", [] ], "index.rst": [ @@ -477627,12 +478011,12 @@ [] ], "pythonpath.rst": [ - "2330356b863cfbfa49b05f78c5da8980b115ead0", + "33eba86b57a509c4e4a932abd8b7ec63e739f1b1", [] ] }, "funcarg_compare.rst": [ - "3bf4527cfb526729d1a9a3184e297f15f534912d", + "8b900d30f2000183f0d07e028ef7300eb15c4550", [] ], "funcargs.rst": [ @@ -477640,11 +478024,11 @@ [] ], "getting-started.rst": [ - "5d13a768069e51169146d4625ee34d6b63c4bd73", + "94e0d80e656d1052ab87e01cf47c476dfb3bf5d1", [] ], "historical-notes.rst": [ - "29ebbd5d1995162b0a57e7d6f5c52ef4b9d302dd", + "5eb527c582ba05241c33fd9e01d4c7929c95966c", [] ], "history.rst": [ @@ -477653,47 +478037,47 @@ ], "how-to": { "assert.rst": [ - "cb70db6b8edf9d24f8d7283f3591b92985d987d3", + "7b027744695846a109a7ad9ad2f58c4b4a0bd88a", [] ], "bash-completion.rst": [ - "245dfd6d9a8f134bc72c6bed28acd04813aea63d", + "117ff7ec13b282775a6df26dc07e73426d8bc296", [] ], "cache.rst": [ - "e7994645dd30a3a81f6b6631379f9d522b10f0a2", + "40cd3f00dd662c9850f6fc9f1aaec7e6a56d82cd", [] ], "capture-stdout-stderr.rst": [ - "9ccea719b6477405168fc3a6155cb72ef29b453a", + "5e23f0c024e7e69e5b8740a4a57d400c9f6f9cab", [] ], "capture-warnings.rst": [ - "065c11e610cfb21c026b03384e3db55ca7ec1bb1", + "afabad5da1438cc70c46bc975de56a3956ed76b1", [] ], "doctest.rst": [ - "ce0b5a5f64960bb0c98a88f21a3ddc6155deefd6", + "c2a6cc8e9586b80116e5ddc165c23c07d1ba95a2", [] ], "existingtestsuite.rst": [ - "9909e7d113a9ed4cf6c6a8f3e9b325acf0723a91", + "1c37023c72a2537b413256e2cc5aaf3a8628d085", [] ], "failures.rst": [ - "ef87550915a189d7ab3b6de19a97232b07254ac8", + "b3d0c155b48fb26f7fb6178ded8e0f948c4f6c91", [] ], "fixtures.rst": [ - "0801387745505a9a08df4f919799eb07e1dc5cb8", + "6cc20c8c3e4cd3a7d824c4570fdb7b6c9ef9e3b5", [] ], "index.rst": [ - "6f52aaecdc3f8b8c32235e88f1899f2bc58c4d46", + "225f289651e0d318438d716e4b380173f3b83eb8", [] ], "logging.rst": [ - "2e8734fa6a3c51437a1f37d9dcfe66aac1ca5ceb", + "300e9f6e6c2418f782f0339d68a2c8c9aa2c2173", [] ], "mark.rst": [ @@ -477701,51 +478085,47 @@ [] ], "monkeypatch.rst": [ - "9c61233f7e5772ae2ac4af20048e7dc5cb59b8c8", - [] - ], - "nose.rst": [ - "4bf8b06c324c7ceac9f32a4838e4004d1bbc9ae6", + "a9504dcb32a8d55e4b330895dea2dfacda66affd", [] ], "output.rst": [ - "4b90988f49d403d7553f013a282a54f25cfe1bc2", + "7a4e32edc78046035b3225830444cb1696b26d19", [] ], "parametrize.rst": [ - "a0c9968428cb84024a34ae7797931cf8b1f2706c", + "b6466c491b4aecb3d3745d3648a5fee83403af55", [] ], "plugins.rst": [ - "cae737e96ed57925e96d97c2eeaef390e21fee95", + "7d5bcd85a3103cbe0b5ce463acd0574d53e9f791", [] ], "skipping.rst": [ - "e2f59c77ae8c019a96f2a25f301d57cdee96d429", + "09a19766f99f704a5685592beae64484f40c24a7", [] ], "tmp_path.rst": [ - "ebd74d42e903d62912aaa02f1436f3759030f4a0", + "3cc5152e992170944d3974d3c205ac6d86ba7e19", [] ], "unittest.rst": [ - "bff7511077808aee6526ebf2559be965e14b1532", + "508aebde01675d8a73e678945c7c269c47a69d85", [] ], "usage.rst": [ - "3522b258dce7fc3f435a26168ce52a4a62955a1a", + "fe46fad2db5d1b1cf43ef2484a8744b89cba5b11", [] ], "writing_hook_functions.rst": [ - "f615fced861de76c515ba89ecd9f41be949f6656", + "f4c00d04fdae9b52855b3bddd397c08af4b01ab2", [] ], "writing_plugins.rst": [ - "b2d2b6563d63ed191fa573235ab1847283d4e0c7", + "4bb6d1833339517922cb81074f3136f6377a0425", [] ], "xunit_setup.rst": [ - "5a97b2c85f13a38d3ca735786d19a8f4dc2e2c81", + "3de6681ff8fb83073c81fd8ad3e6dd0b5ba92932", [] ] }, @@ -477792,7 +478172,7 @@ ] }, "index.rst": [ - "d1b3d2e8a0816343d23fced44ab47212aaada905", + "83eb27b0a53f36e7a324d829cdf4a341ed277a6f", [] ], "license.rst": [ @@ -477800,7 +478180,7 @@ [] ], "naming20.rst": [ - "5a81df2698d2bd59274b36355098a64865e6b3ee", + "112130663843365e78e0c4ae7747a941c20de945", [] ], "proposals": { @@ -477809,10 +478189,6 @@ [] ] }, - "py27-py34-deprecation.rst": [ - "660b078e30ef39f650f27703281742f4a57b8e1c", - [] - ], "pytest.ini": [ "7604360561cc2ae94bc117140b449f76629276e1", [] @@ -477823,7 +478199,7 @@ ], "reference": { "customize.rst": [ - "fe10ca066b25ebfe77e76b1cf58b3fe40e2f90bd", + "cab1117266f1d70f4a17ccb9f971db809a3a51c7", [] ], "exit-codes.rst": [ @@ -477831,24 +478207,24 @@ [] ], "fixtures.rst": [ - "d25979ab95d39eaaac3efe15f956d9344e661070", + "dff93a035efd219295397ea823b513aab1d6c0e2", [] ], "index.rst": [ - "d9648400317afee1f5bd98d5c8ba92487906eabc", + "ee1b2e6214df1e4973e877f7442d8ad0bdc86807", [] ], "plugin_list.rst": [ - "ebf400913690a23bb5a6afdd938021fb258ec987", + "e1d1e3ec24ac1cb35e319fd262e46327662f6de3", [] ], "reference.rst": [ - "0d80c8068071d1029653d6948dfffb37e0b6b72f", + "4036b7d9912dc4927b06885ebfacfbefd8354e42", [] ] }, "requirements.txt": [ - "5b49cb7fccc0773435f8bed6ebe08d349163f375", + "974988c8cf4b5757d37329b6a0d52720a14af7e7", [] ], "sponsor.rst": [ @@ -477856,7 +478232,7 @@ [] ], "talks.rst": [ - "6843c82bab52a50c049b6d836c4bd9c59031d2ae", + "b9b153a792e4f9a95496940a500dee55fd2a91a1", [] ], "tidelift.rst": [ @@ -477871,27 +478247,25 @@ }, "extra": { "get_issues.py": [ - "4aaa3c3ec31c13554ed6cd0f8c78128c4f96dd7e", + "716233ccba178f9458fc8fd7838cac667a4af246", [] - ], - "setup-py.test": { - "setup.py": [ - "d0560ce1f5f4437e574aebc52ddf8b8a37b8605d", - [] - ] - } + ] }, "pyproject.toml": [ - "5d32b755c7445de44bc66a9de76dc817b53e2e82", + "01acfbf7660a56f14e4929d69db4faf63bd23eea", [] ], "scripts": { - "prepare-release-pr.py": [ - "7a80de7edaa2c69044021b91b889408412cf218a", + ".gitignore": [ + "50a75b629590997d2ae9887e3733dbbdbb29fc79", [] ], - "publish-gh-release-notes.py": [ - "68cbd7adffd43aa99bcd473986d10f1c56684d9e", + "generate-gh-release-notes.py": [ + "4222702d5d400e3fd5ea842285fa4f0887bd7f69", + [] + ], + "prepare-release-pr.py": [ + "7dabbd3b328e6178fd5b2efe09fafb1c347302ac", [] ], "release.major.rst": [ @@ -477911,47 +478285,39 @@ [] ], "release.py": [ - "19fef4284285a06b8d9284f4f5191140010cb359", + "bcbc4262d08518cc61f27efb9754d97da2a4bf93", [] ], "towncrier-draft-to-file.py": [ - "81507b40b75fd9fb95f956a1a5429c918e34be8d", + "f771295a01ff11a5d6344e0ddace43151a810a92", [] ], "update-plugin-list.py": [ - "c034c72420b02790173f569f6fa3d0dbec0b5d8c", + "6831fc984dd5ad952fc32c55892ebaae44defb30", [] ] }, - "setup.cfg": [ - "26a5d2e63e56657e73a498ebb5b2dd3cd85bc0c2", - [] - ], - "setup.py": [ - "7f1a1763ca9cebc7bc16576d353d3284ee5d3c7d", - [] - ], "src": { "_pytest": { "__init__.py": [ - "8a406c5c7512bb928f1909cf1f43461cb3efb64c", + "b694a5f244a2828166abc0f55b78a294b64be745", [] ], "_argcomplete.py": [ - "41d9d9407c773bf6a3c4eded476a49649fb80bb2", + "c24f925202a17098b497cdef7f563d93d1f35827", [] ], "_code": { "__init__.py": [ - "511d0dde661dfb97378f76f1d17cbc55e150ef80", + "b0a418e95555ef3e15dc3bc67db9460eb1a00315", [] ], "code.py": [ - "5b758a88480d5e80db59a91fbbb66e30d41ca6f2", + "ee6a5597c2ca8d32b6bdf7c761e14ba6748db985", [] ], "source.py": [ - "208cfb80037a807f81bbb70229c25990c42bb011", + "7fa577e03b34a502d24dfb23150c04c7be8d23b9", [] ] }, @@ -477960,159 +478326,173 @@ "db001e918cb30d25b0e12963cef2544f06bb929f", [] ], + "pprint.py": [ + "75e9a7123b5677e3069b184d9589f222c3714e32", + [] + ], "saferepr.py": [ - "e7ff5cab20368a536cc73eff3ae770bbf899a2b7", + "9f33fced6769a6b47c3a34a61bd1689349be7476", [] ], "terminalwriter.py": [ - "379035d858c92b405c02d13a2bd254ce131d2e5e", + "deb6ecc3c942b567cac1e7115d3426ad78da029b", [] ], "wcwidth.py": [ - "e5c7bf4d8683ccc3866a15ddbc53d8b092d5c05d", + "53803133519fbfbb36cf739181508aff55af4326", + [] + ] + }, + "_py": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "error.py": [ + "ab3a4ed318eac0cab7af09312a5774dc622b0546", + [] + ], + "path.py": [ + "9b4ec68950de1be306782ba034713e72253d7373", [] ] }, "_version.py": [ - "5515abadad7d3102b64fb352af4ff49c92e979ea", + "1df004d997c6d7cd9982c1fca7a4cfe5a8beab1b", [] ], "assertion": { "__init__.py": [ - "480a26ad867181b4d6213e98403230515ba987a9", + "21dd4a4a4bbcc8d5affd3eaa436fbf0a3ac03c7e", [] ], "rewrite.py": [ - "88ac6cab368d117eb65a96de84b966c7f7310e20", + "1e722f2ba15125452b789ec4ec78e755d5cce427", [] ], "truncate.py": [ - "ce148dca095af339fbcf1180c2a892b8a5893e8a", + "4fdfd86a519f9bae070fa0504aec6edaa9065680", [] ], "util.py": [ - "19f1089c20aa1c469802cb4360f79855cb786f14", + "e49c42cfcf744b3a54e493b071863009883a8699", [] ] }, "cacheprovider.py": [ - "681d02b4093be72247d509d085088fd7550563d2", + "a9cbb77cbbb57d2e56828d3cd582a0ec8f3e9494", [] ], "capture.py": [ - "884f035e299e690f15b0b9bc0751e2a306ab9a9f", + "3f6a2510348dd3f1d77e6ff54c49f14fcb099c38", [] ], "compat.py": [ - "7703dee8c5ac952b781c878205e7f4ccfbcba9b7", + "614848e0dbaeb2cfc1356d3393b0d0992922d47f", [] ], "config": { "__init__.py": [ - "ebf6e1b950b80ee79c61463d6f25757d34ef27d8", + "3f46073ac4ad11691d4186a00c55ac775520ea14", [] ], "argparsing.py": [ - "b0bb3f168ff291a45af5ba7b613d21ecd74bfd80", + "9006351af7284d5fefdf30d6703bbf05de692d82", [] ], "compat.py": [ - "ba267d21505f75b10590831f5de0b9cc03fe784f", + "2856d85d19550422abb91ee35f11f461b5b315d7", [] ], "exceptions.py": [ - "4f1320e758d506763261c4bd8dc21453dda4b5ef", + "4031ea732f338ae88d3be522f501da55feb6e2ad", [] ], "findpaths.py": [ - "89ade5f23b91ce1818031a290b1d2f67093ffe96", + "9909376de0f8a8c543b153d3bf291a7fd3ce72d2", [] ] }, "debugging.py": [ - "452fb18ac34f01a96e9df004ad4810976e2c4642", + "6ed0c5c7aeebf6ad6e8aa1fbff02c9554c9c569f", [] ], "deprecated.py": [ - "5248927113ee3e0501de058706d49d30c4fe9a6f", + "10811d158aaeabb88de26166f3c88193b589c953", [] ], "doctest.py": [ - "0784f431b8ed917a20f4cd16fb1be1e8e38751ac", + "7fff99f37b5f92391f75837de5bb02a485176cd0", [] ], "faulthandler.py": [ - "aaee307ff2c6d7d78ea471ab56d806fd349ca853", + "083bcb837399c23a76eeb3cd19fd50440f457d82", [] ], "fixtures.py": [ - "fddff931c513de2b094fca4a027cda460e8dc8c7", + "7fd63f937c19316338fc130d74d36ad6c7cb7f55", [] ], "freeze_support.py": [ - "9f8ea231feddf4168064a1e7b03f36c9adac303e", + "e03a6d1753d816be41a6403ff49c6cc6b52d7e8f", [] ], "helpconfig.py": [ - "aca2cd391e427a6f0cfd7570efcc6d5dc520ae3c", + "37fbdf04d7e1ebb8db2293edbf7a93c4ef6abe52", [] ], "hookspec.py": [ - "79251315d8986f515488981cbea313e74cb383ef", + "9ec9b3b5e1041fad567099b61f431fca18d38209", [] ], "junitxml.py": [ - "4af5fbab0c081d5300c922a2cadf46eebea50c9b", + "13fc9277aeca7374913ac4667b50dc9dd797e328", [] ], "legacypath.py": [ - "37e8c24220e06c53e1994f471bee52a809d3c71c", + "d9de65b1a538631fb0ce9a673b5e3f7ea3f3b234", [] ], "logging.py": [ - "31ad830107610063d38e2cd5dd52d419ac81620d", + "af5e443ced19bc1056ab62029fdd22a2f6687b76", [] ], "main.py": [ - "fea8179ca7d4b80e24df680d2cec215e27910351", + "716d5cf783b38e04cfb7fe5df411ea774f6f2575", [] ], "mark": { "__init__.py": [ - "7e082f2e6e08b4c19bfacdbb30d121e22322f155", + "01d6e7165f2a26b2163626765ad68472356169e2", [] ], "expression.py": [ - "92220d7723aa420f5ec0cb71e602f50f277d798c", + "78b7fda696b57e4edcb6dc2c207802e453e9bf3b", [] ], "structures.py": [ - "0e42cd8de5f58be71804b02f1575e8126502607f", + "a6503bf1d46891eb5763e88db36544cb99ef4340", [] ] }, "monkeypatch.py": [ - "31f95a95ab2266eb955495bc828ff8d024ca594d", + "3f398df76b18041dd546209cdfb1a96577e29112", [] ], "nodes.py": [ - "e49c1b003e0347b60e2a25fdb007be4f812a039d", - [] - ], - "nose.py": [ - "b0699d22bd874a9bb0b1f2cbcac360778b2ea398", + "974d756a2bee88b5bb41dc57a34d2a575a05dadf", [] ], "outcomes.py": [ - "25206fe0e85f1436c0e999b7ca275a4d45c1c4d9", + "f953dabe03d2f341c0005482dca35620fb94a84e", [] ], "pastebin.py": [ - "385b3022cc0e978e656cfc22b3e1b4164a596aae", + "533d78c9a2a4e9f54f8b5a11845e2f3c12830766", [] ], "pathlib.py": [ - "b44753e1a415aa86db47c4c62fb9d3fc3fbcfd26", + "b11eea4e7ef7578cef7f248d1cb8f862607736df", [] ], "py.typed": [ @@ -478120,19 +478500,19 @@ [] ], "pytester.py": [ - "363a3727447bde04e6e09999b07cbd364db8fd5e", + "9ba8e6a8182544fda851e87ef444214506b61bb0", [] ], "pytester_assertions.py": [ - "657e4db5fc318da80a0247be45208c0d4ece38dd", + "d20c2bb599917a3da3aae49e9e5013cbffa56660", [] ], "python.py": [ - "0fd5702a5cc00af7c481854bf7a8027c2fd0cf94", + "41a2fe39af32a6e183395e9a8c60524a25ff4f72", [] ], "python_api.py": [ - "cb72fde1e1f75f54f3c57d92057d3dd30db868cf", + "7d89fdd809ee35c54f365a3fc5c88f6b8c3e6430", [] ], "python_path.py": [ @@ -478140,85 +478520,85 @@ [] ], "recwarn.py": [ - "175b571a80c4f85701c95b881cf98eabdafc7bef", + "63e7a4bd6dc98bdc0044d7c0d2d794c634dc4e1e", [] ], "reports.py": [ - "a68e68bc526f53307eff38fb9e1b33be0545b287", + "70f3212ce7bad14e1e48984ed7c2b31c84651285", [] ], "runner.py": [ - "e43dd2dc818263af055955654ec268202b289a5d", + "d15a682f979076cae29a98d02805755504618594", [] ], "scope.py": [ - "7a746fb9fa9e9c34cf8f697641ca3eef6f3d0224", + "2c6e23208f249dd4900b62567dfed64d45730fbe", [] ], "setuponly.py": [ - "531131ce7262f15c88ff77e4b50723e69e872f12", + "39ab28b466b9c857b64264f76c2903391f11ab6c", [] ], "setupplan.py": [ - "9ba81ccaf0a4f28be0da63bd5881f9cff14a4f1e", + "13c0df84ea1bfd366b405ca632068d8b268a2ccc", [] ], "skipping.py": [ - "ac7216f8385f96da01034ee34e21848329972335", + "188dcae3f1c3f2a78e0182ecc0ba1d458b41cabf", [] ], "stash.py": [ - "e61d75b95f7d93120abbf3266eb52cd1b78aae8e", + "a4b829fc6dd07dd70a64773c7e60640b40b30a4f", [] ], "stepwise.py": [ - "4d95a96b8727fd3a5d91f9497f7bc828efcb6e3a", + "92d3a297e0d3359b6719c59db5ccb1987b8cb323", [] ], "terminal.py": [ - "ccbd84d7d716403c92e43bd6c85c10f711d27def", + "724d5c54d2f160ade5e3333d7e5d704a445d9ee5", [] ], "threadexception.py": [ - "43341e739a0e40cffe3b490dff8e7f20485eb28e", + "09faf661b91eec5ac735fa5765151f6ad6464b1d", [] ], "timing.py": [ - "925163a585840adbc22d54f7fdd055f7d0b520b6", + "0541dc8e0a1b147e64898cd48cd210b96f098ed9", [] ], "tmpdir.py": [ - "f901fd5727c55615814035d78f52e5136c2e391b", + "72efed3e87a175b01354efb73937836697f49e6a", [] ], "unittest.py": [ - "0315168b0446441974bf05007b93e6549b16b786", + "8f1791bf744f15683b2748f20aa1d5c318a7ca8d", [] ], "unraisableexception.py": [ - "fcb5d8237c16e63dac3dff634759a055e4398686", + "f649267abf1076d3409c13ef2fc676b144be814e", [] ], "warning_types.py": [ - "2a97a31978982c5fce5930543d4339bbad86edde", + "a5884f295827f521607b0fa1be76f3a3e1363765", [] ], "warnings.py": [ - "c0c946cbde5c1f65b7c833b85718dbd1c033db6d", + "22590892f8d8ab5116650f9811e50cc936fb8524", [] ] }, + "py.py": [ + "d1c39d203a8e50ef0a22dec74a643268b5669967", + [] + ], "pytest": { "__init__.py": [ - "6050fd112481dab6ecedcb75307d7448e8ef1256", + "c6b6de827e960fa30eee23c400ed661dce4352a3", [] ], "__main__.py": [ - "b170152937b38cda35d4563241288eb3fc27edd0", - [] - ], - "collect.py": [ - "4b2b58180667e286a7c6c7eb1c3baec059561ca3", + "e4cb67d5dd52cd4e47a8bd8d4360c3aa8feb387a", [] ], "py.typed": [ @@ -478228,30 +478608,36 @@ } }, "testing": { + "_py": { + "test_local.py": [ + "1b5b344551c17c63be8d900f8d7c4db4e0fb04b2", + [] + ] + }, "acceptance_test.py": [ - "8b8d4a4a6ed45194fe39ff9a70ebb78281972bff", + "8f001bc240165fcefda3bde9f7abe6c629c8216a", [] ], "code": { "test_code.py": [ - "33809528a06b0b2ef19fa789d91df381257da0e0", + "57ab4cdfddb3dd9af40668948caaa0b2c7e4188b", [] ], "test_excinfo.py": [ - "61aa4406ad26e4e1e8d2f905a214c6bc0e926907", + "e95510f92d676e0f3dd0ed894c2c3c9f68a4ce92", [] ], "test_source.py": [ - "9f7be5e245813706144106e1755b0a2729d20b0c", + "a00259976c44314110352e2ecaec345d94d9f46b", [] ] }, "conftest.py": [ - "107aad86b25b645bc4fb228e3d890a86a635271d", + "b7e2d6111af7fcfbdddffdf0259b268481df47c9", [] ], "deprecated_test.py": [ - "9ac7fe1cacbc5ffffbe3fdc3787318f26a79f104", + "9e83a49d554996536773cc21d4fc3f042d4a3c08", [] ], "example_scripts": { @@ -478265,7 +478651,7 @@ ], "acceptance": { "fixture_mock_integration.py": [ - "5b00ac90e1beaf82b0293daebc44162ab358ce37", + "d802a7f872823c73df765f102237c7e175f7747e", [] ] }, @@ -478277,11 +478663,11 @@ ], "tests": { "__init__.py": [ - "9cd366295e766abed8511b332dcaaea38db7ea31", + "58c41942d1ca12a2d47986d411f505510d7128a8", [] ], "test_foo.py": [ - "8f2d73cfa4f2e3cad4386ec24874b9957e0299e2", + "d88c001c2cc2a3bfb38ffe52fe7f2a60c6bc9f6c", [] ] } @@ -478292,7 +478678,7 @@ [] ], "conftest.py": [ - "973ccc0c030032465d1d959627830a37ff0b944e", + "bba5db8b2fde59403355dfe91cb25ae3762b38df", [] ], "tests": { @@ -478301,7 +478687,7 @@ [] ], "test_basic.py": [ - "f174823854e763f4a425d19fbe28dfb928a16109", + "2809d0cc68956440deda09cdf42468ba150aad86", [] ] } @@ -478309,11 +478695,11 @@ "package_init_given_as_arg": { "pkg": { "__init__.py": [ - "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + "58c41942d1ca12a2d47986d411f505510d7128a8", [] ], "test_foo.py": [ - "f174823854e763f4a425d19fbe28dfb928a16109", + "d88c001c2cc2a3bfb38ffe52fe7f2a60c6bc9f6c", [] ] } @@ -478330,7 +478716,7 @@ [] ], "test_foo.py": [ - "8f2d73cfa4f2e3cad4386ec24874b9957e0299e2", + "d88c001c2cc2a3bfb38ffe52fe7f2a60c6bc9f6c", [] ] } @@ -478341,10 +478727,38 @@ [] ], "conftest.py": [ - "8973e4252d3eaf08fab32ae100af456277392f3e", + "64bbeefac1dc04c5ef727931902f180012c2e8a4", [] ] }, + "customdirectory": { + "conftest.py": [ + "fe1c743a6860cfa9c4d4ad90effb4de86b37a4f8", + [] + ], + "pytest.ini": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "tests": { + "manifest.json": [ + "6ab6d0a522269fbe85a7a0fb9450a11b17ebc5ab", + [] + ], + "test_first.py": [ + "890ca3dea38aa03b37ba8415939cbcea1410bfdb", + [] + ], + "test_second.py": [ + "42108d5da846cc294f7d65e68a9c9e3be4002c91", + [] + ], + "test_third.py": [ + "ede0f3e60256cf24c87979fef044c7dec6a03604", + [] + ] + } + }, "dataclasses": { "test_compare_dataclasses.py": [ "d96c90a91bdd200a2c4f57c4e6546961cad47a13", @@ -478358,8 +478772,16 @@ "4737ef904e07aa8a88b0f3a7d2343f310121155f", [] ], + "test_compare_dataclasses_with_custom_eq.py": [ + "e026fe3d192e7ef78cb27b00e3a84da047696225", + [] + ], + "test_compare_initvar.py": [ + "d687fc22530455c3319e9428e7d7b931bb63485d", + [] + ], "test_compare_recursive_dataclasses.py": [ - "0945790f004201a514da57a064ef69c2b45de86d", + "801aa0a732ed13999549cf89e5da48060ceb8012", [] ], "test_compare_two_different_dataclasses.py": [ @@ -478370,11 +478792,11 @@ "doctest": { "main_py": { "__main__.py": [ - "e471d06d643b0febf822a7ba4083bf8416cd7205", + "c8a124f54160cf5e2b33bad537ba7fb656cda579", [] ], "test_normal_module.py": [ - "700cc9750cfab5c6ee94ce74a7968efa7ffebb61", + "26a4d90bc89c8c94cffa0e4d988a05076e491831", [] ] } @@ -478386,7 +478808,7 @@ [] ], "conftest.py": [ - "a7a5e9db80a34a63bea27558cd4e89d94250893c", + "fe1ae620aa63626e9ca80b86ef8bf4892a8fb726", [] ], "foo": { @@ -478395,7 +478817,7 @@ [] ], "test_foo.py": [ - "f174823854e763f4a425d19fbe28dfb928a16109", + "2809d0cc68956440deda09cdf42468ba150aad86", [] ] } @@ -478408,11 +478830,11 @@ [] ], "conftest.py": [ - "be5adbeb6e536ae8435b21ce07a954c59640ed89", + "3a5d3ac33fe2641aeef2e5c03fbdd7b0b19b7949", [] ], "test_in_sub1.py": [ - "df36da1369b3538c480cbe20dfa4da096bb0d80f", + "d0c4bdbdfd903ab10ed9ad9cf8483ca69bf1dffd", [] ] }, @@ -478422,17 +478844,17 @@ [] ], "conftest.py": [ - "00981c5dc12b2d8f848fbe368b3dc278ffdb1006", + "a1f3b2d58b91cbe5bb2d8b9630777895235894e6", [] ], "test_in_sub2.py": [ - "1c34f94acc478114b177c4f1e4daaf1402f8de8f", + "45e9744786a4b27983d592d4c7898c87642e839e", [] ] } }, "test_detect_recursive_dependency_error.py": [ - "d1efcbb338c27575fedd2e3b5b04a71bd0a10212", + "84e5256f070536c3e42e7f7f945bd59f6d7d3e47", [] ], "test_extend_fixture_conftest_conftest": { @@ -478441,7 +478863,7 @@ [] ], "conftest.py": [ - "5dfd2f779579c732f249d12b379fd7f97e5dc830", + "7f1769beb9b99267206d511cea49d0ca60284dd9", [] ], "pkg": { @@ -478450,11 +478872,11 @@ [] ], "conftest.py": [ - "4e22ce5a1373df2082dcfd35b6ea4370c5213fe7", + "ad26fdd8cdc624d7204413f3a2b1c9e055148fa9", [] ], "test_spam.py": [ - "0d891fbb503f8c819640151f2ba9f876cf7e2737", + "9ee74a471863cda1ffa55c08211b4af159b91db7", [] ] } @@ -478465,41 +478887,41 @@ [] ], "conftest.py": [ - "5dfd2f779579c732f249d12b379fd7f97e5dc830", + "7f1769beb9b99267206d511cea49d0ca60284dd9", [] ], "test_extend_fixture_conftest_module.py": [ - "46d1446f4702c88e86c0006f2a13675e22f12f36", + "fa688f0a844867ea12bd9985d00988cd996e1372", [] ] }, "test_extend_fixture_module_class.py": [ - "87a0c894111517cdff2110df363498912b04f33d", + "f78a57c322b9f16fee94a1e7472c7a2a05ac7616", [] ], "test_funcarg_basic.py": [ - "0661cb301fc12ab2fde5e7460f96e6c878c2dee1", + "12e0e3e91d47b31e2fdab6920a8ef9f353bb146f", [] ], "test_funcarg_lookup_classlevel.py": [ - "256b92a17ddfd372f1249ef78710684f63f82703", + "8b6e8697e06066f95a51e98e8f3b3042ba3999b9", [] ], "test_funcarg_lookup_modulelevel.py": [ - "e15dbd2ca45de7a89145102be7b6098fea8f6b5f", + "40587cf2bd1e29e261397af9db28a3661d7b183b", [] ], "test_funcarg_lookupfails.py": [ - "b775203231f88cc78733a54be52ff39dd7b898a6", + "0cc8446d8eec20394a2031248725d1c0ae1f86c3", [] ] }, "test_fixture_named_request.py": [ - "75514bf8b8c53e468a032c26b312b5db973646b5", + "a2ab7ee330d38cf2533f4190bdb2241a0b9c2245", [] ], "test_getfixturevalue_dynamic.py": [ - "055a1220b1c9293a00049ab395011872782db1ee", + "0f316f0e449840fb33b3e5998aa9e02aaed6db7f", [] ] }, @@ -478509,16 +478931,16 @@ [] ], "conftest.py": [ - "cb8f5d671ea8599f258a6029fa466af869954948", + "bde5c0711ace579929de67f49c952accf77b588b", [] ], "test_hello.py": [ - "56444d147480fdc344d70aa2087050cb69acd27b", + "dd18e1741f01e54860dbc7a56761ab5dfde7874e", [] ] }, "issue_519.py": [ - "e44367fca04687d38b1ae0f601b706ab7490ed9c", + "39766164490f0d47cff1646bd3513917eb8d3957", [] ], "junit-10.xsd": [ @@ -478536,7 +478958,7 @@ [] ], "test_marks_as_keywords.py": [ - "35a2c7b76289639ebccf00e8699861214a273f3c", + "d95ad0a83d9f37517888eab9cb5b2003d5243168", [] ] } @@ -478548,11 +478970,11 @@ [] ], "generate_folders.py": [ - "ff1eaf7d6bb87cf5dbe641dca6004e0ace6dd058", + "17085e50b54e8e83ec6326fb9bef6274cbc2d2e3", [] ], "template_test.py": [ - "064ade190a10c88c9779c3cff0ccca199db7138b", + "f50eb65525c8885c699af78e6795a73ace5cbcc5", [] ] } @@ -478563,52 +478985,52 @@ ], "tmpdir": { "tmp_path_fixture.py": [ - "8675eb2fa620d74bd723b2528ec303810850cdaf", + "4aa35faa0b6565479782f7ba51ab6bfad6d04f4f", [] ] }, "unittest": { "test_parametrized_fixture_error_message.py": [ - "d421ce927c944893a35d9631b77736f952954542", + "d66b66df5b73d5a391cac4881f423cd1e1d16e01", [] ], "test_setup_skip.py": [ - "93f79bb3b2e24cb5cf02fd98f45381a9e92a0da5", + "7550a0975769d1c58e8e0806132877f78f26dcdf", [] ], "test_setup_skip_class.py": [ - "4f251dcba17819a0876f612304f96c12ce3986e4", + "48f7e476f40ae39e173e0e3e66103291cf6fef23", [] ], "test_setup_skip_module.py": [ - "98befbe510fd3eff81251c761bc769131b7cae1e", + "eee4263d22be7f1f91d11ae8980e8bd3e86af61d", [] ], "test_unittest_asyncio.py": [ - "1cd2168604c495539376fc2f2031d97c7e729543", + "a82ddaebcc3c3884a8828ea75c4037f1f1a189ca", [] ], "test_unittest_asynctest.py": [ - "fb26617067c72f88416a25f56573a316c74f5871", + "e9b10171e8d2d70fb298b95efd2bed1a92628723", [] ], "test_unittest_plain_async.py": [ - "78dfece684ebb90594bf70168c1c1b6360fd6af9", + "2a4a66509a4cd886153f04849f813d86f5e238a0", [] ] }, "warnings": { "test_group_warnings_by_message.py": [ - "6985caa4407f39e1ad3d8381714b5408a8619fc8", + "be64a1ff2c8017e927b877ad70b264a8524ac6fc", [] ], "test_group_warnings_by_message_summary": { "test_1.py": [ - "b8c11cb71c983b99778dab720f9d47d4d04dda43", + "95fa795efe0476752026d8a3c5f54fcefac2e361", [] ], "test_2.py": [ - "636d04a5505acf1540b641bcb373537ac03966f3", + "5204fde8a85bd30cfb8270a7b22b519d1acd2321", [] ] } @@ -478626,11 +479048,11 @@ [] ], "create_executable.py": [ - "998df7b1ca727833356fff725850f33c97d48a8a", + "fbfda2e5d9448acb2248e4930037f91d274eb2c2", [] ], "runtests_script.py": [ - "591863016acea45dee428cd65d8016bc3809d9f8", + "ef63a2d15b9d3ec4cd0dcff7bf4602718e148021", [] ], "tests": { @@ -478639,32 +479061,36 @@ [] ], "test_trivial.py": [ - "08a55552abbf6252e8505ea9dffbdd32ece5b4c5", + "425f29a649c760f304eec9ea8eacc76731481e6d", [] ] }, "tox_run.py": [ - "678a69c858a2cd4094307acf90e6e996c9800ee3", + "7fd63cf1218f2a9d78219de2b3d10de09e21b2ad", [] ] }, "io": { + "test_pprint.py": [ + "15fe6611280d8e3addf0f60c370c1d782d3c777e", + [] + ], "test_saferepr.py": [ - "63d3af822b17ab9d241a50ec89ab2fcf9ec29de9", + "5d270f1756c594e144c9e06a9eebbd881feb394a", [] ], "test_terminalwriter.py": [ - "4866c94a55884398d462c59d062cafad748c7176", + "afa8d5cae871cfc50d1f967372b8cd4593e84d02", [] ], "test_wcwidth.py": [ - "7cc74df5d0791306f45e0e95b8225eb900ef6959", + "82503b8300c29051a6ed931c865ffdeeb4cc492c", [] ] }, "logging": { "test_fixture.py": [ - "bcb20de5805574bb098fb67c9936d544ea2586f2", + "c1cfff632afa1c7c59fb1150c77e75397a052f45", [] ], "test_formatter.py": [ @@ -478672,7 +479098,7 @@ [] ], "test_reporting.py": [ - "323ff7b2446a31ecc2e20e96be9cc77c5a238b54", + "7e592febf56fa49fd754b39fa4725fbb0ff51e98", [] ] }, @@ -478690,7 +479116,7 @@ [] ], "bdd_wallet.py": [ - "35927ea5875f11e727f17d9cea11c23a00e95f7d", + "2bdb15454241f4226806d366d490384624ad6c0a", [] ], "django_settings.py": [ @@ -478698,61 +479124,61 @@ [] ], "pytest.ini": [ - "b42b07d145aba3a906dfca2c18d85a2331f4335a", + "3bacdef62ab9426a9338c2c07688d073ec8931ca", [] ], "pytest_anyio_integration.py": [ - "65c2f59366344777da31251c45e81cfbc377dc65", + "383d7a0b5db3d64a4b267d072e125baf28a4f216", [] ], "pytest_asyncio_integration.py": [ - "5d2a3faccfcc7b3bd0ed6d9f5e292be9dcacf27f", + "b216c4beecd6cd871de21784584b5475669fa777", [] ], "pytest_mock_integration.py": [ - "740469d00fb97c517f36b460613698b6a8228fcd", + "5494c44270addb0a72cd71c17e6c40d72ac08867", [] ], "pytest_trio_integration.py": [ - "199f7850bc47c72fd8a678f9630088345cf43b97", + "60f48ec609b3e9b811d66349045f424ba26ca6e0", [] ], "pytest_twisted_integration.py": [ - "94748d036e5de5424dd81903a1d7697267d5abe4", + "0dbf5faeb8a19287e4a3a875313331bd7ca86ff2", [] ], "requirements.txt": [ - "90b253cc6d310a7a1e4f1411511a3aed86c3e23c", + "9e152f1191bbb0263996f9f844e2b43865cbd941", [] ], "simple_integration.py": [ - "20b2fc4b5bb7eb4e481183dec6d288524aefcddb", + "48089afcc7e959507dc47acf0340edf42f3cde32", [] ] }, "python": { "approx.py": [ - "0d411d8a6daadcffd7369af9965abbc221df3475", + "968e88285121ee17774c2695db20fd538a7f9808", [] ], "collect.py": [ - "ac3edd395ab83f401766a988a745648c3ef1fa2c", + "745550f0775a91414dfba8e2749a63d410893b9c", [] ], "fixtures.py": [ - "f29ca1dfa59049e4edac4bf980dfb1c6b5fdd4e9", + "741cf7dcf42cc729ab9405319ad9bb13e954c907", [] ], "integration.py": [ - "d138b726638ed9cee9b8a41272c9b404df7c8182", + "c20aaeed83995cde527c4f57feded89de4fb4797", [] ], "metafunc.py": [ - "fc0082eb6b98c07f66a5f280a5cd256ec1ecd3be", + "3d0058fa0a764c1370ffcd9cbf903bac411386cf", [] ], "raises.py": [ - "2d62e91091b128e5de0f97add1ca30a9ddf76b7b", + "929865e31a099ed1d836b66b8bd3802c7194e126", [] ], "show_fixtures_per_test.py": [ @@ -478761,159 +479187,155 @@ ] }, "test_argcomplete.py": [ - "8c10e230b0c8a233f580617fee286b4f676a7b6c", + "0c41c0286a460bdf608a0d445ec6affb894e0f11", [] ], "test_assertion.py": [ - "2516ff1629e82730a10774468d05a0ef04530166", + "ef4e36644d9da7017d8d4773cb96918296410e3f", [] ], "test_assertrewrite.py": [ - "4417eb4350fef5581b693d88842afad619b7ee91", + "ac93c57dbd74ae1c3a4d1883d09136cecd95c983", [] ], "test_cacheprovider.py": [ - "cc6d547dfb1b54c06414d4fbf486c65f64d5e01f", + "ea662e87f0788bcde42ab87b665a33039f604dfb", [] ], "test_capture.py": [ - "1bc1f2f8db2cdd0a0808b89b8990e4e941fd47ea", + "0521c3b6b04857f265cd9870a638e88531c1bce6", [] ], "test_collection.py": [ - "6a8a5c1cef11eb0e7a396c8c13b0cb26b5c267a8", + "7f0790693a521c5facfd762b7d71c2241a861a7c", [] ], "test_compat.py": [ - "37cf4a077d783716df0df93f6d71d0be3cc9db89", + "73ac1bad8586d370cc1d55a8210faa551b2d01ed", [] ], "test_config.py": [ - "8013966f07161c4e70f06de1a78564beaf68a2d7", + "0d097f71622b27b8a4f745333ac21f34c99f77e5", [] ], "test_conftest.py": [ - "64c1014a53347f41326b032df9ae5edc0fb66343", + "06438f082a9340fd0e179df11fd1a184d5200e84", [] ], "test_debugging.py": [ - "a822bb57f58673b08ead1160f00bca91ecce3710", + "7582dac67429a6d10bd71bbee97b3b73c504b1a2", [] ], "test_doctest.py": [ - "67b8ccdb7ecd23809195461e98c0ee36e2c1050a", + "a95dde9a6bfb72488c2d37933739b8d69f1292a8", [] ], "test_entry_points.py": [ - "5d003127363dd4e37f70dcdd40291f94319b2866", + "68e3a8a92e49b3314cba7d633d5a36a007aa325d", [] ], "test_error_diffs.py": [ - "1668e929ab413642e1bb0148b971f4a6cc5f364f", + "f290eb1679f81a43df10d86b30b0f449ea6e5778", [] ], "test_faulthandler.py": [ - "5b7911f21f833e74356357e9d23a9599483f914b", + "a3363de98169349bdd7ec286e5781cc053237891", [] ], "test_findpaths.py": [ - "3a2917261a255063b1c306e3b1a8c566e5b42325", + "260b9d07c9c44697c9403c8927a6ce2f0ebfc782", [] ], "test_helpconfig.py": [ - "44c2c9295bf3557510441c8233883c3901cc0d9e", + "4906ef5c8f02fa16dbc8880f85101feb56fec428", [] ], "test_junitxml.py": [ - "02531e81435cfec743090d1fdfc10c2b8529b518", + "86edfbbeb612ebbc7d931c992f1400d445600a78", [] ], "test_legacypath.py": [ - "8acafe98e7c4faaff76342d39d660f8e6a659ea1", + "ad4e22e46b4d499d26232d9015b70cdc84265e78", [] ], "test_link_resolve.py": [ - "60a86ada36e56821b99f329a4dbdd5fa91fb0ec6", + "0461cd75554cfa224ddd0adf9dd3ea35dddec84f", [] ], "test_main.py": [ - "2df51bb7bb9992061bcb9dd132c1cf0cefe4efe2", + "6294f66b360cdef366877888d310304fe6b9254c", [] ], "test_mark.py": [ - "da67d1ea7bcd6394f2e7504afab3c8fd3b5057c8", + "2896afa453211133fef2175f6e8af35c54bcfb2a", [] ], "test_mark_expression.py": [ - "f3643e7b4098d4f4b74b8a925ef93831be831022", + "07c89f90838257c2080fc33a6d646a619079a25f", [] ], "test_meta.py": [ - "9201bd21611207f20b41fd0a91af70bdc4f9ac94", + "40ed95d6b47a67dfbc0e30dc7c2eb64d97c2c790", [] ], "test_monkeypatch.py": [ - "95521818021050594a1949cad1f76584b2931671", + "12be774beca43bd300de30c3fbf11511b0e6a422", [] ], "test_nodes.py": [ - "df1439e1c49cccb22197537453cc265b698200ae", - [] - ], - "test_nose.py": [ - "1ded8854bb780a9ad7e5a41056b5d16afcb401c4", + "a3caf471f707544150606de152e08c72281b9d50", [] ], "test_parseopt.py": [ - "28529d04378324be9097dd274a66a244b178fb6d", + "e959dfd631bc6641584013dce48cf3ebe69fa5d1", [] ], "test_pastebin.py": [ - "b338519ae17b16c580932599ebf560c09543b34c", + "651a04da84aaefc78cf86dafd6a9f52c13ed8893", [] ], "test_pathlib.py": [ - "5eb153e847df4d2355d69855dcd9ce7fdf16d3d5", + "688d13f2f053ff8440397e7e15d914985f2bea9a", [] ], "test_pluginmanager.py": [ - "9fe23d177923075dbf113f85b1d246ee58f38982", + "99b003b66ed4b50884fed693c15f40eee2c9c43b", [] ], "test_pytester.py": [ - "049f8b22d81b3550f6a592813dc02088ede86e88", + "9c6081a56dbb9f93ecbb6b4dbf7651443fe7d129", [] ], "test_python_path.py": [ - "5ee0f55e36a335447fda42c1d4477a63c19b8b0d", + "73a8725680f6821a811e6550516ba39c4cb3caba", [] ], "test_recwarn.py": [ - "d3f218f1660adb7d529e0b2228706b0d7aefd8f7", + "27ee9aa72f073f309a11033764e19f5638c9e6c1", [] ], "test_reports.py": [ - "31b6cf1afc6aa02a34f7e3432c9267041f30842c", + "c6baeebc9dd376f6352c2ffd655ec637469f60a6", [] ], "test_runner.py": [ - "2e2c462d978962b3103ecbb1630ea209a722c4da", + "99c11a3d92c749dec3759e45dbe8baa1db226ea3", [] ], "test_runner_xunit.py": [ - "e077ac41e2c2e7b012835750c0e2c9d2bea96635", + "587c9eb9fefbc1f3dd3b721bd3eea7adf33b5c08", [] ], "test_scope.py": [ - "09ee1343a8068de8729b6be100ebf0f8a0e37d12", + "1727c2ee1bbb4c3ebc4975aea7889a705ce4fdfa", [] ], "test_session.py": [ - "3ca6d390383c0819bc38126be2ea863d5d2fbe9f", + "8624af478b140630700b0818fab4fc05f4f6f4b4", [] ], "test_setuponly.py": [ - "fe4bdc514eb6a735e6239309cf9c8f2775bcc131", + "8638f5a61403a693c8c9dd9078393a3c1abe1cf2", [] ], "test_setupplan.py": [ @@ -478921,52 +479343,52 @@ [] ], "test_skipping.py": [ - "3010943607c22d7987f6538b832632d501faf90f", + "a1511b26d1cca9df98b8498ee38ee82c930e9892", [] ], "test_stash.py": [ - "2c9df4832e443b17fda95533a47b9d4a6d7c48e8", + "e523c4e6f2b0833f88b74565237b7df0ae96e8cc", [] ], "test_stepwise.py": [ - "63d29d6241ff89f551ccd8b7c667701f6d6ec006", + "472afea66207a0b9e0cdeb0211d7b22490b57840", [] ], "test_terminal.py": [ - "23f597e33251f2f2b09531bef000ff5f657b5106", + "5ed0fee82e675cbba64fba58df7f4d121e1d37f2", [] ], "test_threadexception.py": [ - "5b7519f27d87a83dcd562f2f6ce7f53de5947a3d", + "99837b94e8a5f732b17a10661b8a631fd660fa19", [] ], "test_tmpdir.py": [ - "4f7c5384700d0b8f63ef4b70b18431cf5ae99e31", + "331ee7da6c7c1e88ba853eefc58d30d435631e1d", [] ], "test_unittest.py": [ - "1601086d5b20c3b788245901d51478a2c3e6790b", + "96223b22a2ea84ae327d19034078985c2393182c", [] ], "test_unraisableexception.py": [ - "f625833dceaca49c0026f0dd0691482891ac0264", + "1657cfe4a84eb61533780626ad2ee55504685347", [] ], "test_warning_types.py": [ - "b49cc68f9c6990fc0127a20c6e81e8346c5866c0", + "a50d278bde277d139d7e11a033e59bffd989a57b", [] ], "test_warnings.py": [ - "5663c46cead1844efca129e8865606680a464539", + "3ef0cd3b5469372e274de8eef68b55df82351f94", [] ], "typing_checks.py": [ - "0a6b5ad284104d37fb04bd9ae78cb5a26469a97d", + "4b146a25110032fbd7e1d4e3ea6db8aec9cad591", [] ] }, "tox.ini": [ - "b2f90008ce1219eca5c5f554b946cb7f5e3bb335", + "30d3e68defc4bba480688e3ee9bcee12e967d3f5", [] ] }, @@ -481015,7 +481437,7 @@ [] ], "capture.py": [ - "75717d62c8a90874e24cd26c888bd15951167348", + "78da63be417f225d34f06a4e41b237dd848b8955", [] ], "commandline.py": [ @@ -482381,7 +482803,7 @@ [] ], "chrome_ios.py": [ - "20fcb9088988987ebf04f4abcf6e028f5b3096c6", + "db81b91957b0639097ce33dd666716c13efced90", [] ], "chrome_spki_certs.py": [ @@ -482501,7 +482923,7 @@ [] ], "executorwebdriver.py": [ - "3e221b897981eb32227df1dfe57d6713859c775b", + "f985d4867591cf13a02a17b5f58e3033628fce3f", [] ], "executorwktr.py": [ @@ -483168,7 +483590,7 @@ [] ], "test_stash.py": [ - "4157db5726d4f92c767c469b77f8105ed5f3c04d", + "7806dc1c9b2f0a452da1b03dae8113dc9c1c4920", [] ] }, @@ -484075,7 +484497,7 @@ [] ], "urltestdata.json": [ - "b9a199daab660cef5d208103797c554f1868e508", + "53f6d575e165bf34aadddf23a7df0aa4b0f65031", [] ] }, @@ -485953,7 +486375,7 @@ }, "browsing_context": { "__init__.py": [ - "51b3c64b42d3e2527d93445f3bc308d0105e2528", + "91899eb50df49b1ab47e55ab52c2b49bd2a4fe0e", [] ], "activate": { @@ -486302,7 +486724,7 @@ [] ], "serviceworker.html": [ - "c7ca7448d052898462db02e9a58fc5029eb8a777", + "a1f50e731b23d4c41827b6bcc49f8cd5b4e6e080", [] ], "serviceworker.js": [ @@ -487335,6 +487757,10 @@ "a67e708ae20b0e0d0b39fd55685be068ccd162a6", [] ], + "gelu.json": [ + "414227975913f31b95f9c4446f5acd94be6d76fb", + [] + ], "gemm.json": [ "23a18b316caab6b9531cface498d36808ec4796d", [] @@ -487500,7 +487926,7 @@ [] ], "softmax.json": [ - "ebb12114fc6e08ed51b1f9dd3ff820d3664e8f2f", + "cbe868ac1dee4cfa85454b5c785daa8be2aaad39", [] ], "softplus.json": [ @@ -487545,7 +487971,7 @@ ] }, "utils.js": [ - "3cfcf579a50e4658f7cb0fbe0306fa84cbaea240", + "b8a017fb44bfe4748f7ab7c78aff3333373baf1d", [] ], "utils_validation.js": [ @@ -490377,7 +490803,7 @@ ], "resources": { "helpers.js": [ - "ee77fd8d30d713eb5e764db51c0edd1429e0f1e8", + "9a2b90549ba91e39be49ad4efe622fbe290c3a12", [] ] } @@ -494218,60 +494644,83 @@ {} ] ], - "idbcursor_advance_index.htm": [ - "4c0c0b697a30149b17a20705232040a9051f88ba", - [ - null, - {} - ] - ], - "idbcursor_advance_index2.htm": [ - "04842f71c31410a282706b612c6160c4cd06649d", - [ - null, - {} - ] - ], - "idbcursor_advance_index3.htm": [ - "a797286e7c1a3be0a676e9f9102d8e0815e3a919", - [ - null, - {} - ] - ], - "idbcursor_advance_index5.htm": [ - "5e60aefd5a556412f295322f09d85bcab2c55c77", + "idbcursor_advance_index.any.js": [ + "c928bdabd5f589b3c9613721c44850ca39e25bf2", [ - null, - {} - ] - ], - "idbcursor_advance_index6.htm": [ - "3a1168f57a5af9f9c209b755b97bb0ca1ce6cedd", - [ - null, - {} - ] - ], - "idbcursor_advance_index7.htm": [ - "f5ecc6144c234f056786a6f165c72979ecaf8ea2", + "IndexedDB/idbcursor_advance_index.any.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "title", + "IDBCursor.advance()" + ], + [ + "script", + "resources/support.js" + ] + ] + } + ], [ - null, - {} - ] - ], - "idbcursor_advance_index8.htm": [ - "1f9ff9c453ae393ad77b2c111c8208cc2e3d7926", + "IndexedDB/idbcursor_advance_index.any.serviceworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "title", + "IDBCursor.advance()" + ], + [ + "script", + "resources/support.js" + ] + ] + } + ], [ - null, - {} - ] - ], - "idbcursor_advance_index9.htm": [ - "1756419c6d254f072e198b4cd96c46260314a751", + "IndexedDB/idbcursor_advance_index.any.sharedworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "title", + "IDBCursor.advance()" + ], + [ + "script", + "resources/support.js" + ] + ] + } + ], [ - null, - {} + "IndexedDB/idbcursor_advance_index.any.worker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "title", + "IDBCursor.advance()" + ], + [ + "script", + "resources/support.js" + ] + ] + } ] ], "idbcursor_advance_objectstore.any.js": [ @@ -496549,7 +496998,7 @@ ] ], "idbobjectstore_getAll.any.js": [ - "db7098f71e80b214b25bf3f0d42563c937f29918", + "600d000788963a6d626db014a43a9c2ffc5e654c", [ "IndexedDB/idbobjectstore_getAll.any.html", { @@ -515797,6 +516246,15 @@ } ] ], + "async-svg-read-write.tentative.https.html": [ + "78c814a1f1a240609946aea0f5c40a4c5022c213", + [ + null, + { + "testdriver": true + } + ] + ], "async-unsanitized-html-formats-write-read.tentative.https.html": [ "848900830f136dd82abef9a564080a70b85f5f44", [ @@ -527045,7 +527503,7 @@ ] ], "secure.https.html": [ - "9308899694375520f93f5a85a696d09d6940b0d8", + "4557cd248fb207b01f65d3630f6c0e3d5a66eaa3", [ null, { @@ -527947,7 +528405,7 @@ ] ], "fedcm-use-other-account-button-flow.tentative.https.html": [ - "996523af84727a32240f03c3c3ac2963bf4ee4f2", + "3b90782713f5829e87f753fbf349c2a700862443", [ null, { @@ -527956,7 +528414,7 @@ ] ], "fedcm-use-other-account.tentative.https.html": [ - "2022bbc0f72c7d2fc41e1ce608b0a10eb4c2f729", + "96006cce68c802c7df7addfc7040d00e71f63b99", [ null, { @@ -531415,6 +531873,13 @@ {} ] ], + "try-tactic-back-to-base.html": [ + "9a11fc7fffc90c1852846dc2cf8352599d1d4c38", + [ + null, + {} + ] + ], "try-tactic-base.html": [ "b52c3d15e0f231dc384f538e785d24935544a082", [ @@ -535454,7 +535919,7 @@ ] ], "container-for-shadow-dom.html": [ - "c3ae9612546bf8d7f64519634e95b83f89fa386c", + "63d58f76982e872896c1819f9c9f090918705664", [ null, {} @@ -536035,7 +536500,7 @@ ] ], "style-container-for-shadow-dom.html": [ - "d88bf0bca98292c05c6503190c623fa45614fcb6", + "e8297be941f2be6ab488f20ad4dad62772a492c4", [ null, {} @@ -553374,6 +553839,13 @@ {} ] ], + "serialize-escape-identifiers.html": [ + "90476d6eca0ac6ccfa0e9644272afc6b79a50d07", + [ + null, + {} + ] + ], "trailing-braces.html": [ "ac2e8e61b9942d96493269b18f7dc2d0860ff3aa", [ @@ -557651,7 +558123,7 @@ ] ], "transform-with-sign-function.html": [ - "64b30dca17ee4853a467f535a9c90d4438763455", + "96ba054fbef3f097b2720b8dea785dbf70be5e27", [ null, {} @@ -558311,7 +558783,7 @@ ] ], "starting-style-cascade.html": [ - "cef3e88b656d1d65d3e02179c6f44e878fd246d3", + "921ba6b20047e66470db62b31231dc2c163775f8", [ null, {} @@ -563435,7 +563907,7 @@ }, "css-viewport": { "computedStyle-zoom.html": [ - "82af111bbe72d3ab20f071d97ff5919781061add", + "bd53ce535ceb416456e0b6237ba8b81d5542e4a8", [ null, {} @@ -566288,6 +566760,13 @@ {} ] ], + "smooth-scroll-nonstop.html": [ + "7ba6e690f9072671db98a442239b5fff1fa92e8f", + [ + null, + {} + ] + ], "subpixel-sizes-and-offsets.tentative.html": [ "d198b9dde60c8cac16241c412d3e55f772166010", [ @@ -570376,8 +570855,8 @@ ] }, "digital-credentials": { - "digital-credentials.tentative.https.html": [ - "30e24e5450a463d59a09ccf9150c15170b85e901", + "identity-get.tentative.https.html": [ + "ba5212e433b42eb6a19e5a137aaedcc7869e75eb", [ null, { @@ -571251,7 +571730,7 @@ ] ], "EventTarget-constructible.any.js": [ - "b0e7614e625b3de018eb76c90148c7710b6c807f", + "4125d23f0c965066c91d95c9ba83ac711fdabbc3", [ "dom/events/EventTarget-constructible.any.html", {} @@ -574541,28 +575020,21 @@ }, "parts": { "basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html": [ - "58db5cd04ff3d6b211f0f84552203fa03aa61404", + "b2782dbfa9bcd95007cc1855521e2e4d1c464c7a", [ null, {} ] ], "basic-dom-part-declarative-brace-syntax.tentative.html": [ - "001ac24a445d890ec47fbf9d81cb11ec10b2ffe4", - [ - null, - {} - ] - ], - "basic-dom-part-declarative-pi-syntax.tentative.html": [ - "f6460353949a9c4984dccacc36e54c05bad17f53", + "3a4370387373a364a8ac818aee6cbb0e97fe0c6d", [ null, {} ] ], "basic-dom-part-objects.tentative.html": [ - "d7834fe69b0118eac16a372dd7b9c14cdd57e575", + "f2199362c4d734b24d3f5edc4a2a0c8aec5ea32f", [ null, {} @@ -575293,6 +575765,34 @@ {} ] ], + "evaluator-cross-realm.tentative.html": [ + "5bc1128658c4f5a89574f9ad9a2030562ea4fbec", + [ + null, + {} + ] + ], + "evaluator-different-document.tentative.html": [ + "8092eef46245ee18f2095033320a844ac78e69c4", + [ + null, + {} + ] + ], + "expression-cross-realm.tentative.html": [ + "f75a949e730eb0fb43956d75fd900cbf99f97b07", + [ + null, + {} + ] + ], + "expression-different-document.tentative.html": [ + "6e35d8b02670b1129ee52575fdf1d7e48f6880b8", + [ + null, + {} + ] + ], "fn-concat.html": [ "fe160966aa7983191e81ffa65f5955c86afe7bb9", [ @@ -596489,7 +596989,7 @@ ] ], "keepalive.any.js": [ - "d6ec1f67920717d65e9dd550abebbb106fb1688a", + "55225e00aa061309102757a24e3d0729e81aac05", [ "fetch/api/basic/keepalive.any.html", { @@ -610422,6 +610922,69 @@ } }, "file-system-access": { + "getDirectory.https.any.js": [ + "bace6860b85cfbb170b4b87982de20af86d86f3b", + [ + "file-system-access/getDirectory.https.any.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "script", + "resources/test-helpers.js" + ] + ] + } + ], + [ + "file-system-access/getDirectory.https.any.serviceworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "script", + "resources/test-helpers.js" + ] + ] + } + ], + [ + "file-system-access/getDirectory.https.any.sharedworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "script", + "resources/test-helpers.js" + ] + ] + } + ], + [ + "file-system-access/getDirectory.https.any.worker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ], + [ + "script", + "resources/test-helpers.js" + ] + ] + } + ] + ], "idlharness.https.any.js": [ "ca3e92061a2a5a09f04c9548d9163bba8f39521d", [ @@ -623138,9 +623701,9 @@ ] ] }, - "geolocation-API": { + "geolocation": { "PositionOptions.https.html": [ - "6b36d66d7358cccd6fe5143a1504a17db4170af4", + "e54e8679aa9d16cca2c83ac4aba50c286355010e", [ null, { @@ -623224,7 +623787,7 @@ "idlharness.https.window.js": [ "eadd6b7a4fbce9a286821bb9ad4243292fb6458a", [ - "geolocation-API/idlharness.https.window.html", + "geolocation/idlharness.https.window.html", { "script_metadata": [ [ @@ -623248,7 +623811,7 @@ ] ], "non-fully-active.https.html": [ - "7b381be7fff68ecf42425af168c560f51f818842", + "20fcdae12a04469ed1204f95667508815cbbcde6", [ null, { @@ -623264,7 +623827,7 @@ ] ], "permission.https.html": [ - "4062843349d77263fad2d8e12c6558f5d715ac04", + "aa46caf32c3470cb5bc65ef22d9ef1e41bdbbfcc", [ null, {} @@ -670868,6 +671431,13 @@ } ] ], + "option-computed-style.tentative.html": [ + "14650f3f320ae6f1974558ef63f47298a67801b3", + [ + null, + {} + ] + ], "select-accessibility-minimum-target-size.tentative.html": [ "364efd1554219493a5b9262c18a12317b9571037", [ @@ -685399,7 +685969,7 @@ ] ], "input-events-typing.html": [ - "a894beea9bd381313b999e45abe0b4b8f61c804b", + "cff32a44d153145a02c0914ef22d2fefea0edd97", [ null, { @@ -691865,7 +692435,14 @@ }, "mediacapture-extensions": { "GUM-backgroundBlur.https.html": [ - "605a4e0831914834ac4a5587bbdb7e87cde74e59", + "5383e088a62abc8e9f5abbbf0f941c3d460a30c6", + [ + null, + {} + ] + ], + "GUM-eyeGazeCorrection.https.html": [ + "3235b166d843eaf94296d22c4c6597f8733ba453", [ null, {} @@ -698434,6 +699011,32 @@ } ] ], + "fetch-url-resolve.https.window.js": [ + "4d2e9940cb8684be8aec056b393f2449b43e950c", + [ + "notifications/fetch-url-resolve.https.window.html", + { + "script_metadata": [ + [ + "script", + "/resources/testdriver.js" + ], + [ + "script", + "/resources/testdriver-vendor.js" + ], + [ + "script", + "/service-workers/service-worker/resources/test-helpers.sub.js" + ], + [ + "script", + "resources/helpers.js" + ] + ] + } + ] + ], "getnotifications-across-processes.https.window.js": [ "ad0ecf27efd6b9856a4936386c2b303f6fc6df9b", [ @@ -703063,6 +703666,15 @@ } ] ], + "pointerevent_after_target_removed_from_slot.html": [ + "170c1ec2f1c9b98abcfdeee51c2e43463489ce01", + [ + "pointerevents/pointerevent_after_target_removed_from_slot.html?mouse", + { + "testdriver": true + } + ] + ], "pointerevent_after_target_removed_interleaved.tentative.html": [ "ec6ffce9268e1636f5b878fd16361d2273212cb8", [ @@ -703974,6 +704586,27 @@ {} ] ], + "pointerevent_to_slotted_target.html": [ + "cea318f20d492548cf66aafe856176ecd2e07142", + [ + "pointerevents/pointerevent_to_slotted_target.html?mouse", + { + "testdriver": true + } + ], + [ + "pointerevents/pointerevent_to_slotted_target.html?pen", + { + "testdriver": true + } + ], + [ + "pointerevents/pointerevent_to_slotted_target.html?touch", + { + "testdriver": true + } + ] + ], "pointerevent_touch-action-auto-css_touch.html": [ "c486d77c1119128d7ad87c5c83aea43ebfe379e7", [ @@ -704602,7 +705235,7 @@ ] ], "preconnect-onerror-event.html": [ - "4ce583d4db8502d626c5c563c078b36fa08a768d", + "b2997fed2536f724310b183a141d21d10c501edf", [ null, { @@ -705028,6 +705661,27 @@ } }, "private-aggregation": { + "private-aggregation-permissions-policy-default.https.sub.html": [ + "81e0c241184b6b43d5c00bcc51ff1cd5e59a5d15", + [ + null, + {} + ] + ], + "private-aggregation-permissions-policy-none.https.sub.html": [ + "a10efe95c4bb7fe0406e028f4aeebce940ff0ff8", + [ + null, + {} + ] + ], + "private-aggregation-permissions-policy-self.https.sub.html": [ + "0ec4c6b400dd6519f45acd96f03fff3632d6d51b", + [ + null, + {} + ] + ], "protected-audience-auction-report-buyers-debug-mode-surface.https.html": [ "ddc2b6a3723071d7b0f21b0f17757a1f1c59e1c8", [ @@ -705055,13 +705709,6 @@ } ] ], - "shared-storage-permissions-policy-none.https.html": [ - "3593ed71ea6b1e755c8c5e71492eb46699496922", - [ - null, - {} - ] - ], "shared-storage-surface-context-id.https.html": [ "3b0e1b3d740ad56382ace7946304709181e50b39", [ @@ -718124,7 +718771,7 @@ ] ], "content-type.html": [ - "f6b1db7d9f8e7133de365d2f40bc05057be13070", + "5a09a9114f2c28ba316098194027ec77df05e3f4", [ null, {} @@ -722348,7 +722995,7 @@ ] ], "scroll-to-text-fragment.html": [ - "73931d4b0e617dcf07fd50f90c15a83dc664d277", + "91f282f849f54fe8a80bae08c0cfaa65d047eca1", [ null, { @@ -727178,6 +727825,15 @@ {} ] ], + "focus-slot-box-generated-tabindex-0.html": [ + "6406254167c3ec8a52bf6891deab0348a817c7a1", + [ + null, + { + "testdriver": true + } + ] + ], "focus-tab-on-shadow-host.html": [ "0dffc0157f53d7178ebb0ff04b3b09977642b280", [ @@ -727510,8 +728166,37 @@ ], "reading-order": { "tentative": { + "flex-flow.html": [ + "d69f95e2afcf077a333332c13f99788a8ebd5d7c", + [ + null, + { + "testdriver": true, + "timeout": "long" + } + ] + ], + "flex-visual-order.html": [ + "ce91f9b3c4c7e618e493d852ca4a861c86d1d774", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-columns.html": [ + "f07dc63bc58b9c03b3a80ac1a0cfa6073fdfb319", + [ + null, + { + "testdriver": true, + "timeout": "long" + } + ] + ], "grid-order-across-scopes.html": [ - "d505383e33899e86b4820b03943fecdba07b68b1", + "81d1b7de539596a144ad75c77f95c321090880db", [ null, { @@ -727576,6 +728261,16 @@ "timeout": "long" } ] + ], + "grid-rows.html": [ + "f24427468435b47c800cfd5a1ba390d270e1d2b8", + [ + null, + { + "testdriver": true, + "timeout": "long" + } + ] ] } } @@ -728682,21 +729377,21 @@ ] ], "select-url-permissions-policy-default.tentative.https.sub.html": [ - "67911388ec0f41790d8c7c678256f153765a6294", + "a5be825d0e80f776127c58bcf0f43e5538c489a7", [ null, {} ] ], "select-url-permissions-policy-none.tentative.https.sub.html": [ - "6820edb0843863257aa2de3a8741d08383a13eab", + "6b48036fadb0820d445ecac4d98021237b31d795", [ null, {} ] ], "select-url-permissions-policy-self.tentative.https.sub.html": [ - "b79bc065c214235d219fbf880baf556e7b030f3c", + "71d5653a2d180083fea0ba7ff567cab8c3697db7", [ null, {} @@ -728740,21 +729435,21 @@ ] ], "shared-storage-permissions-policy-default.tentative.https.sub.html": [ - "5439df2a0684331e63059e095396a9b2af89c17f", + "d0ff76a61efd17d498bcef4d19dcc2c32f3357f2", [ null, {} ] ], "shared-storage-permissions-policy-none.tentative.https.sub.html": [ - "17a4bf1803ca33edb2d0259d14f484839c9aa3e5", + "c3cd3b1b47851ddecc42cf140f98deef7dd5136e", [ null, {} ] ], "shared-storage-permissions-policy-self.tentative.https.sub.html": [ - "171325cdf8f0a909eac659e1dcbb334e50eef216", + "9c45e86b67ab88b432367de86519b918a3cd82b0", [ null, {} @@ -731650,6 +732345,40 @@ } ] ], + "restriction-media-capabilities-decoding-info.https.html": [ + "6bcb63b597896b9c0323dfb1bef3ba61eb8c3d02", + [ + "speculation-rules/prerender/restriction-media-capabilities-decoding-info.https.html?target_hint=_blank", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "speculation-rules/prerender/restriction-media-capabilities-decoding-info.https.html?target_hint=_self", + { + "testdriver": true, + "timeout": "long" + } + ] + ], + "restriction-media-capabilities-encoding-info.https.html": [ + "3eb5950ee78df82d017d954bf4d791e4c7565db0", + [ + "speculation-rules/prerender/restriction-media-capabilities-encoding-info.https.html?target_hint=_blank", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "speculation-rules/prerender/restriction-media-capabilities-encoding-info.https.html?target_hint=_self", + { + "testdriver": true, + "timeout": "long" + } + ] + ], "restriction-media-device-info.https.html": [ "f725145aa1592222d516df1dc5d4e8b46a7b9153", [ @@ -733026,7 +733755,7 @@ ] ], "opaque-origin.https.window.js": [ - "cc1d31fdf2c68322f72a40b1e84ec05ab1930779", + "b9539760db7c42b3079bac97efeb7f252b54ccaa", [ "storage/opaque-origin.https.window.html", { @@ -733034,6 +733763,18 @@ [ "title", "StorageManager API and opaque origins" + ], + [ + "script", + "/resources/testdriver.js" + ], + [ + "script", + "/resources/testdriver-vendor.js" + ], + [ + "script", + "resources/helpers.js" ] ] } @@ -733142,33 +733883,34 @@ } ] ], - "storagemanager-persist-persisted-match.https.any.js": [ - "edbe67fae2c1e17bc7ce3aa3c3d1dc9db7982cac", + "storagemanager-persist-persisted-match.https.window.js": [ + "9a4e0d329fcff1b5370ca8e63f1e2052a6130d52", [ - "storage/storagemanager-persist-persisted-match.https.any.html", + "storage/storagemanager-persist-persisted-match.https.window.html", { "script_metadata": [ [ "title", "StorageManager: result of persist() matches result of persisted()" - ] - ] - } - ], - [ - "storage/storagemanager-persist-persisted-match.https.any.worker.html", - { - "script_metadata": [ + ], [ - "title", - "StorageManager: result of persist() matches result of persisted()" + "script", + "/resources/testdriver.js" + ], + [ + "script", + "/resources/testdriver-vendor.js" + ], + [ + "script", + "resources/helpers.js" ] ] } ] ], "storagemanager-persist.https.window.js": [ - "13e17a16e14198e21b0b5984ed46c93a50daed41", + "1bcbefd8a96f4e680e0c31a40c8b457818411215", [ "storage/storagemanager-persist.https.window.html", { @@ -733176,6 +733918,18 @@ [ "title", "StorageManager: persist()" + ], + [ + "script", + "/resources/testdriver.js" + ], + [ + "script", + "/resources/testdriver-vendor.js" + ], + [ + "script", + "resources/helpers.js" ] ] } @@ -742972,6 +743726,13 @@ {} ] ], + "SVGGraphicsElement.getBBox-04.html": [ + "2b5a5eb2bb9310670b37805a6daf7c35e9101e0a", + [ + null, + {} + ] + ], "SVGGraphicsElement.getScreenCTM.html": [ "7e5dfc649f89112cb8b4d31352a5b1e8788739fa", [ @@ -743458,6 +744219,13 @@ {} ] ], + "Element-toggleAttribute.html": [ + "c3b141f8cbd1557221fabc192ebc0b75505b8c87", + [ + null, + {} + ] + ], "GlobalEventHandlers-onclick.html": [ "0fdde778cc8863600ecdcc57a6d6281b52a777d4", [ @@ -743983,14 +744751,14 @@ ] ], "trusted-types-eval-reporting-no-unsafe-eval.html": [ - "2b0922d212379cf7e7803fbe24cb3007884270c0", + "7a31866719ff09c93330724bddba4f276b893931", [ null, {} ] ], "trusted-types-eval-reporting-report-only.html": [ - "4e8ac5a2f43c97740269901fceb65db6b02752a5", + "0e7cf34bb14cd9b11ccc9f969bfa55171d92d315", [ null, {} @@ -744039,7 +744807,7 @@ ] ], "trusted-types-reporting.html": [ - "9db307db60d963308ef970790da8e6cb70b02550", + "42759093604dedd525d6b1069b959b0394d4605d", [ null, { @@ -744255,7 +745023,7 @@ ] ], "keyboard-accesskey-click-event.html": [ - "f90101e31efc4e21643dccfaadb9f20658ecee6e", + "93363c2e55784c423cec5926caa5c4e912bc7119", [ null, { @@ -765724,6 +766492,225 @@ }, "webnn": { "conformance_tests": { + "add.https.any.js": [ + "60fef8e0b4adb2521a150eacf5d5c8559a2d86d3", + [ + "webnn/conformance_tests/add.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/add.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/add.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/add.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/add.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/add.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "arg_min_max.https.any.js": [ "dd6cd6d9181abe7070e34376b678df6d5204f076", [ @@ -766163,7 +767150,7 @@ ] ], "buffer.https.any.js": [ - "9b4c43198b4df239b2bf5af91ec436ee49188314", + "bc838ee7680cea828baa27b0dd1483573ff4188a", [ "webnn/conformance_tests/buffer.https.any.html?cpu", { @@ -766190,6 +767177,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -766226,6 +767217,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -766262,6 +767257,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -766298,6 +767297,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -766334,6 +767337,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -766370,6 +767377,10 @@ ], [ "script", + "../resources/utils_validation.js" + ], + [ + "script", "../resources/utils.js" ], [ @@ -767884,10 +768895,10 @@ } ] ], - "elementwise_binary.https.any.js": [ - "0124077773db1bffcfd2e8208376878876eb7ca8", + "div.https.any.js": [ + "65438e6c519de4bf56819cbfa4ec34a16cb3e49f", [ - "webnn/conformance_tests/elementwise_binary.https.any.html?cpu", + "webnn/conformance_tests/div.https.any.html?cpu", { "script_metadata": [ [ @@ -767923,7 +768934,7 @@ } ], [ - "webnn/conformance_tests/elementwise_binary.https.any.html?gpu", + "webnn/conformance_tests/div.https.any.html?gpu", { "script_metadata": [ [ @@ -767959,7 +768970,7 @@ } ], [ - "webnn/conformance_tests/elementwise_binary.https.any.html?npu", + "webnn/conformance_tests/div.https.any.html?npu", { "script_metadata": [ [ @@ -767995,7 +769006,7 @@ } ], [ - "webnn/conformance_tests/elementwise_binary.https.any.worker.html?cpu", + "webnn/conformance_tests/div.https.any.worker.html?cpu", { "script_metadata": [ [ @@ -768031,7 +769042,7 @@ } ], [ - "webnn/conformance_tests/elementwise_binary.https.any.worker.html?gpu", + "webnn/conformance_tests/div.https.any.worker.html?gpu", { "script_metadata": [ [ @@ -768067,7 +769078,7 @@ } ], [ - "webnn/conformance_tests/elementwise_binary.https.any.worker.html?npu", + "webnn/conformance_tests/div.https.any.worker.html?npu", { "script_metadata": [ [ @@ -769198,6 +770209,225 @@ } ] ], + "gelu.https.any.js": [ + "67287c3ad3f1983b419e6b5f98126cd5577b5876", + [ + "webnn/conformance_tests/gelu.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gelu.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gelu.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gelu.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gelu.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gelu.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "gemm.https.any.js": [ "0af471753e049fe8984e9c047c3b1d9555914e49", [ @@ -770950,6 +772180,663 @@ } ] ], + "max.https.any.js": [ + "2281f3ed0a15a0f46f1e92de0f942c1fea81d70e", + [ + "webnn/conformance_tests/max.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/max.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/max.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/max.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/max.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/max.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], + "min.https.any.js": [ + "5d12a1d7e2cb5b79012bf3b1f88191f96c24380e", + [ + "webnn/conformance_tests/min.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/min.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/min.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/min.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/min.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/min.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], + "mul.https.any.js": [ + "94ead346449debe79f99d5a7589f22521a1baedf", + [ + "webnn/conformance_tests/mul.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/mul.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/mul.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/mul.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/mul.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/mul.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "pad.https.any.js": [ "ec9c106e6330fe399c99e63d523f611b8f4c31ee", [ @@ -771607,6 +773494,225 @@ } ] ], + "pow.https.any.js": [ + "c988e92d91775179eb429fbb463d3752eec347c9", + [ + "webnn/conformance_tests/pow.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/pow.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/pow.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/pow.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/pow.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/pow.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "prelu.https.any.js": [ "3b4717663fa86ed33f38e09eade394f29b5a833a", [ @@ -773141,7 +775247,7 @@ ] ], "softmax.https.any.js": [ - "75893eb34cb208ead47f2ec2595e798e6828b235", + "20c050d7bd8e5c4788e799c0991e7e4dcadd338e", [ "webnn/conformance_tests/softmax.https.any.html?cpu", { @@ -774016,6 +776122,225 @@ } ] ], + "sub.https.any.js": [ + "367780e1147f9b6a845ead3fb0e162605c843ed1", + [ + "webnn/conformance_tests/sub.https.any.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/sub.https.any.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/sub.https.any.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/sub.https.any.worker.html?cpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/sub.https.any.worker.html?gpu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/sub.https.any.worker.html?npu", + { + "script_metadata": [ + [ + "title", + "test WebNN API element-wise binary operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "variant", + "?cpu" + ], + [ + "variant", + "?gpu" + ], + [ + "variant", + "?npu" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "tanh.https.any.js": [ "d7d1b5975189091bbe1cf6ffef4b836ca8bf2831", [ @@ -774894,7 +777219,7 @@ ] }, "idlharness.https.any.js": [ - "93e88294ef1ed0347db722a9c1305aac47fc4129", + "c2f579a2b4f58e6806d603c9201e7f98c2fe8409", [ "webnn/idlharness.https.any.html", { @@ -775077,7 +777402,7 @@ ] ], "clamp.https.any.js": [ - "126fa90e167fb344d2b41f71d85f1c3f4ed8addd", + "ce1394802ed24def6ddfec262752533adbb728f9", [ "webnn/validation_tests/clamp.https.any.html", { @@ -775651,7 +777976,7 @@ ] ], "gru.https.any.js": [ - "50d39d297a3961b00e671d9e019c999bd987cdfb", + "b1b576e96d070387e36f609f360122bd81f8e10a", [ "webnn/validation_tests/gru.https.any.html", { @@ -775692,7 +778017,7 @@ ] ], "gruCell.https.any.js": [ - "3cd9d32b07397a37be3e1907e8e4fdf151b1048b", + "0465928d514fab705c710707fb89463d8ea100e1", [ "webnn/validation_tests/gruCell.https.any.html", { @@ -776020,7 +778345,7 @@ ] ], "lstm.https.any.js": [ - "18d609798ce93aeeb3e30b9c5ed983bc2fa36917", + "c7d341a9d5aa29ab9ca77f8344439c258e6092dd", [ "webnn/validation_tests/lstm.https.any.html", { @@ -776061,7 +778386,7 @@ ] ], "lstmCell.https.any.js": [ - "c3769c828d42528dfd947fb892fe0df69cb72769", + "d4031b07b24e61054828cd3ff84ceead68ad12bb", [ "webnn/validation_tests/lstmCell.https.any.html", { @@ -776571,7 +778896,7 @@ ] ], "softmax.https.any.js": [ - "68891b27d88074ea80b13a9ff93c47030f30766c", + "a75878307f620f672ff0aecc4d7649c8e4a3162b", [ "webnn/validation_tests/softmax.https.any.html", { @@ -777600,10 +779925,12 @@ ] ], "RTCRtpParameters-codecs.html": [ - "97f519a45e4c97b5a9a191d6beb73f5faa7e1a6c", + "933677f792b84568b00e1c9a852d7e8fcd2b9e4e", [ null, - {} + { + "timeout": "long" + } ] ], "RTCRtpParameters-encodings.html": [ @@ -777642,7 +779969,7 @@ ] ], "RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html": [ - "d728ec5a9c9758e0b754ef754e881b6aa5e0f7c3", + "005286497cb10598582024684a24df84e331f3cd", [ null, { @@ -777697,7 +780024,7 @@ ] ], "RTCRtpReceiver-video-jitterBufferTarget-stats.html": [ - "022dbe70c50b97757ca82cfc38a062d63393c73d", + "d69d7f51990df8f73b3a09569b389407172de9dc", [ null, { @@ -778027,7 +780354,7 @@ ] ], "idlharness.https.window.js": [ - "98685f1cd12378e715b5b35317387abea137ba00", + "58e696f73c15a0c5fd2f38e625577096df0af318", [ "webrtc/idlharness.https.window.html", { @@ -778477,6 +780804,15 @@ } ] ], + "setParameters-maxFramerate.https.html": [ + "e404787dbd8ace1445a3f860d735cf0593d57821", + [ + null, + { + "timeout": "long" + } + ] + ], "vp8.https.html": [ "3d04bc71726e719dbed700cfc72d301924b73030", [ @@ -796091,16 +798427,6 @@ } ] ], - "multi-screen-window-open-fullscreen.tentative.https.html": [ - "008c53d4e749c6550b1824c69bf1467f7fe759c7", - [ - null, - { - "testdriver": true, - "timeout": "long" - } - ] - ], "multi-screen-window-open.tentative.https.html": [ "708e571c27f950ed414d5e5ea9981bfdec93880a", [ @@ -799028,7 +801354,7 @@ ] ], "shared-worker-options-mismatch.html": [ - "604f69a43671137edbd2672fb417d1b342d86b51", + "76beda937fbd12e45c41e7c44fde2c769cb7b55f", [ null, {} @@ -821520,6 +823846,148 @@ ] ] }, + "css-overflow": { + "overflow-alignment-block-001.html": [ + "20192eb2f10ab73a4107b3ddb78efed55f4a127c", + [ + null, + {} + ] + ], + "overflow-alignment-block-002.html": [ + "587f7394449fb053c5554f1f5a28b4a559badc90", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-reverse-001.html": [ + "30cb6c92db74a6aacd675b2e06cc8c29942ff804", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-reverse-002.html": [ + "a6252391c49ec2fc27f5214322363823329c26a1", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-reverse-overflow-001.html": [ + "0a98dae690bec44df55734ab0a04d42e1045cf85", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-reverse-overflow-002.html": [ + "d5c842e4b70116aaaadbc520d2d3031e3a59b2d4", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-wrap-001.html": [ + "b980b94314c5056f4a33fb98a83144f807cc593c", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-wrap-002.html": [ + "195a877ccd467c0df08af47005ae7191c3a0c0de", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-wrap-overflow-001.html": [ + "59815fa0aa93a6d25aa448673eb0965f476b09d9", + [ + null, + {} + ] + ], + "overflow-alignment-flex-col-wrap-overflow-002.html": [ + "cb0ce12b9e9a3032b314e246e0a363e6435d4b37", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-reverse-001.html": [ + "c41e7ce3e5262c7ce90216160ffd4375d8ee4f87", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-reverse-002.html": [ + "a032d59e6758b01d049e1a084da8bb145dfb7d1b", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-reverse-overflow-001.html": [ + "fbc0ef02bf776996f8ab6738ef6eb6ad34e337db", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-reverse-overflow-002.html": [ + "a1b9f4bae8664769050440d55dd6919c333f94ec", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-wrap-001.html": [ + "51f02add772c8c0614016994561e304ad41daf74", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-wrap-002.html": [ + "e5ee089d0c4ffb0342a9dcaf4e74bab64cbe5429", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-wrap-overflow-001.html": [ + "63f5ef2c8ea3d03e3ba3361f065d088037981d11", + [ + null, + {} + ] + ], + "overflow-alignment-flex-row-wrap-overflow-002.html": [ + "e1d3fb2c7d91ce0c21fe70b8ce9f988c140ba1d9", + [ + null, + {} + ] + ], + "overflow-alignment-grid-001.html": [ + "bf98b62116da30f43e3be503ec90edf06714c794", + [ + null, + {} + ] + ], + "overflow-alignment-grid-002.html": [ + "76d3886d7c6a1213dc15c9e252f20a8557ed9f21", + [ + null, + {} + ] + ] + }, "css-round-display": { "polar-anchor-center-001.html": [ "2af9546a0c916790ed2cb5a7cc5f5f135043a885", @@ -823873,21 +826341,21 @@ ] ], "reference_context.py": [ - "6b7fd8b2be1b1c1051d4f68df56c087066db86eb", + "8511495594b40d00ab4a3a90a0788ab0848020af", [ null, {} ] ], "type.py": [ - "cc6d7e51ab6863654b51e9afd10209c69117012d", + "17703d60123e28571919cd87d5c90143f4bad7bf", [ null, {} ] ], "user_context.py": [ - "51406262fc85e431be241570595dfc5992628188", + "490188c265d461d285772bb6c9c79215b6b5ff6d", [ null, {} @@ -823921,7 +826389,7 @@ }, "get_tree": { "frames.py": [ - "81c664740c1e8c11ae3c798f302b6e32881117c5", + "4fd220ed8fc4426f540bf9eedad9e1d8ac399a05", [ null, {} @@ -823935,14 +826403,14 @@ ] ], "max_depth.py": [ - "ca1d0edfa1b8225c9e603a56e097ec97e4098630", + "b855b8e7eba06b56a63ee2dd4f64b0e88d8a7513", [ null, {} ] ], "root.py": [ - "74d11c600330fb6ce05dee24fa306a8a597f843d", + "40e9f8ac93b81b5c499133db9682798683725131", [ null, {} @@ -824462,7 +826930,7 @@ }, "integration": { "cookies_with_network_events.py": [ - "e7fddbb1c47142f41ad2233b27722e314485e482", + "16ca358e23be51d2de3047fd28ec47b66ece4163", [ null, {} @@ -824624,14 +827092,14 @@ ] ], "cookies.py": [ - "eb79529ab1c8a2570d212b8b2f2868f080151dce", + "5f594c4b029f0bd4abf58f574d70dee2f03b7632", [ null, {} ] ], "headers.py": [ - "11106c7720883d5f9bfe5b95163b01e19de641f5", + "1a7acfcde005c44d5becdf6495ce72c32ff3c7be", [ null, {} diff --git a/tests/wpt/meta/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini b/tests/wpt/meta/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini new file mode 100644 index 00000000000..1b96343c8a3 --- /dev/null +++ b/tests/wpt/meta/css/css-backgrounds/background-size/background-size-contain-svg-view.html.ini @@ -0,0 +1,2 @@ +[background-size-contain-svg-view.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini b/tests/wpt/meta/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini new file mode 100644 index 00000000000..bd7b19d36fd --- /dev/null +++ b/tests/wpt/meta/css/css-backgrounds/background-size/background-size-cover-svg-view.html.ini @@ -0,0 +1,2 @@ +[background-size-cover-svg-view.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/font-variant-emoji-003.html.ini b/tests/wpt/meta/css/css-fonts/font-variant-emoji-003.html.ini new file mode 100644 index 00000000000..0553b106708 --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/font-variant-emoji-003.html.ini @@ -0,0 +1,2 @@ +[font-variant-emoji-003.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/font-variant-emoji-004.html.ini b/tests/wpt/meta/css/css-fonts/font-variant-emoji-004.html.ini new file mode 100644 index 00000000000..61006a4ff60 --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/font-variant-emoji-004.html.ini @@ -0,0 +1,2 @@ +[font-variant-emoji-004.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-lists/counters-006.html.ini b/tests/wpt/meta/css/css-lists/counters-006.html.ini new file mode 100644 index 00000000000..4ab1181c04a --- /dev/null +++ b/tests/wpt/meta/css/css-lists/counters-006.html.ini @@ -0,0 +1,2 @@ +[counters-006.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-001.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp-001.tentative.html.ini deleted file mode 100644 index 25535f43bd2..00000000000 --- a/tests/wpt/meta/css/css-overflow/line-clamp-001.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-001.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-004.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp-004.tentative.html.ini deleted file mode 100644 index 079d68f9db0..00000000000 --- a/tests/wpt/meta/css/css-overflow/line-clamp-004.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-004.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-010.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp-010.tentative.html.ini deleted file mode 100644 index 197075b7e1a..00000000000 --- a/tests/wpt/meta/css/css-overflow/line-clamp-010.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-010.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-015.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp-015.tentative.html.ini deleted file mode 100644 index e397f59fad1..00000000000 --- a/tests/wpt/meta/css/css-overflow/line-clamp-015.tentative.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[line-clamp-015.tentative.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-001.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-001.html.ini new file mode 100644 index 00000000000..039be87e913 --- /dev/null +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-001.html.ini @@ -0,0 +1,2 @@ +[line-clamp-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-004.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-004.html.ini new file mode 100644 index 00000000000..5ea4302a5de --- /dev/null +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-004.html.ini @@ -0,0 +1,2 @@ +[line-clamp-004.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-005.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-005.tentative.html.ini index f7e5bd89633..f7e5bd89633 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-005.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-005.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-006.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-006.tentative.html.ini index a20fcc3b242..a20fcc3b242 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-006.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-006.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-007.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-007.tentative.html.ini index 650254666e8..650254666e8 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-007.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-007.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-008.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-008.tentative.html.ini index 01050849b00..01050849b00 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-008.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-008.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-009.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-009.tentative.html.ini index c5fde4b81a4..c5fde4b81a4 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-009.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-009.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-010.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-010.html.ini new file mode 100644 index 00000000000..ffb496aa0c0 --- /dev/null +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-010.html.ini @@ -0,0 +1,2 @@ +[line-clamp-010.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-011.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-011.tentative.html.ini index 801b5a840df..801b5a840df 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-011.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-011.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-012.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-012.tentative.html.ini index a7fd0303bee..a7fd0303bee 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-012.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-012.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-013.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-013.tentative.html.ini index 3f398e788d2..3f398e788d2 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-013.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-013.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-015.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-015.html.ini new file mode 100644 index 00000000000..0619b41ed9a --- /dev/null +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-015.html.ini @@ -0,0 +1,2 @@ +[line-clamp-015.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-016.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-016.tentative.html.ini index 8c13ceb8691..8c13ceb8691 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-016.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-016.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-017.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-017.tentative.html.ini index e98e35fd17a..e98e35fd17a 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-017.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-017.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-019.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-019.tentative.html.ini index baf2cf8f33d..baf2cf8f33d 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-019.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-019.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-001.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html.ini index 0846054622b..0846054622b 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-001.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-002.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html.ini index 672bbf776fe..672bbf776fe 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-002.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-003.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html.ini index 8fdd292cb00..8fdd292cb00 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-003.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-004.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html.ini index 29770e6524a..29770e6524a 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-004.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-005.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html.ini index 7c7c26bce5c..7c7c26bce5c 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-005.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-009.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html.ini index 8e3126191e7..8e3126191e7 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-009.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-011.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html.ini index 55e2d3799e9..55e2d3799e9 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-011.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-013.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html.ini index 080b13f055b..080b13f055b 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-013.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-014.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html.ini index 83b70019122..83b70019122 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-014.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-015.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html.ini index 96067a1553b..96067a1553b 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-015.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-auto-016.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html.ini index bc8e676d3fb..bc8e676d3fb 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-auto-016.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-001.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html.ini index 96277819670..96277819670 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-001.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-002.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html.ini index 21efb29c1f9..21efb29c1f9 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-002.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-003.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html.ini index 189ac0f3620..189ac0f3620 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-003.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-004.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html.ini index 4e4d8534002..4e4d8534002 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-004.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-005.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html.ini index 4a2b7b2ba98..4a2b7b2ba98 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-005.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-006.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html.ini index bd697fabf3f..bd697fabf3f 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-006.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-007.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html.ini index b63c81cd54e..b63c81cd54e 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-007.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-008.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html.ini index 834446721a7..834446721a7 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-008.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-009.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html.ini index 24f8b5bded3..24f8b5bded3 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-009.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-010.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html.ini index b4d85b57450..b4d85b57450 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-010.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-011.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html.ini index b399afd2326..b399afd2326 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-abspos-011.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-001.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html.ini index b3a7f12f4eb..b3a7f12f4eb 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-001.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-002.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html.ini index 73ed3ce5449..73ed3ce5449 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-002.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-003.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html.ini index daeb119383d..daeb119383d 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-003.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-004.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html.ini index ee7aa7cffa8..ee7aa7cffa8 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-004.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-005.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html.ini index 0321cf6b3d7..0321cf6b3d7 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-005.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-006.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html.ini index 9cb5ff1b379..9cb5ff1b379 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-006.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-007.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html.ini index 547cac7a2ca..547cac7a2ca 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-007.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-008.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html.ini index 23716f1941f..23716f1941f 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-008.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-009.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html.ini index 1f92c2b17f5..1f92c2b17f5 100644 --- a/tests/wpt/meta/css/css-overflow/line-clamp-with-floats-009.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-005.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-005.html.ini index fd3b482cd4c..fd3b482cd4c 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-005.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-005.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-006.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-006.html.ini index 37a36c18ed0..37a36c18ed0 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-006.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-006.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-007.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-007.html.ini index ee3feba6326..ee3feba6326 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-007.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-007.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-008.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-008.html.ini index d7d111177a0..d7d111177a0 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-008.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-008.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-009.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-009.html.ini index 2eeee791cf0..2eeee791cf0 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-009.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-009.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-010.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-010.html.ini index eb78fb115b3..eb78fb115b3 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-010.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-010.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-011.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-011.html.ini index 9ffffc40f10..9ffffc40f10 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-011.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-011.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-012.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-012.html.ini index d65978f7379..d65978f7379 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-012.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-012.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-013.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-013.html.ini index 80db2cbf4b0..80db2cbf4b0 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-013.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-013.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-014.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-014.html.ini index 10e4bd44fd1..10e4bd44fd1 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-014.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-014.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-016.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-016.html.ini index 7787a8caf58..7787a8caf58 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-016.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-016.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-017.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-017.html.ini index 30fed79972d..30fed79972d 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-017.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-017.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-020.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-020.html.ini index 920a915cbfd..920a915cbfd 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-020.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-020.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-021.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-021.html.ini index d4c6b4fa7ee..d4c6b4fa7ee 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-021.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-021.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-024.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-024.html.ini index 7958cc24372..7958cc24372 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-024.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-024.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-025.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-025.html.ini index f882cda18a2..f882cda18a2 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-025.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-025.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-027.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-027.html.ini index 0768495e834..0768495e834 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-027.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-027.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-030.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-030.html.ini index 15501f962a9..15501f962a9 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-030.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-030.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-031.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-031.html.ini index b676c4ceb75..b676c4ceb75 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-031.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-031.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-032.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-032.html.ini index 46b3b9509f5..46b3b9509f5 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-032.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-032.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-035.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-035.html.ini index fd300a1ade2..fd300a1ade2 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-035.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-035.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-036.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-036.html.ini index f2716aacf89..f2716aacf89 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-036.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-036.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-040.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-040.html.ini index b3a2caf391e..b3a2caf391e 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-040.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-040.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-043.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-043.html.ini index 85656176cc9..85656176cc9 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-043.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-043.html.ini diff --git a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html.ini b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html.ini index aacd5624a5d..aacd5624a5d 100644 --- a/tests/wpt/meta/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html.ini +++ b/tests/wpt/meta/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html.ini diff --git a/tests/wpt/meta/css/css-transforms/transform-with-sign-function.html.ini b/tests/wpt/meta/css/css-transforms/transform-with-sign-function.html.ini index acc2f9ef2d3..a4c4b9fa419 100644 --- a/tests/wpt/meta/css/css-transforms/transform-with-sign-function.html.ini +++ b/tests/wpt/meta/css/css-transforms/transform-with-sign-function.html.ini @@ -28,3 +28,6 @@ [calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) should be used-value-equivalent to 2 2 2] expected: FAIL + + [calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2deg) should be used-value-equivalent to 2 2 2 2deg] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/rlh-unit-001.html.ini b/tests/wpt/meta/css/css-values/rlh-unit-001.html.ini new file mode 100644 index 00000000000..60dbcb52aed --- /dev/null +++ b/tests/wpt/meta/css/css-values/rlh-unit-001.html.ini @@ -0,0 +1,2 @@ +[rlh-unit-001.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/vh_not_refreshing_on_chrome.html.ini b/tests/wpt/meta/css/css-values/vh_not_refreshing_on_chrome.html.ini new file mode 100644 index 00000000000..26435e28b09 --- /dev/null +++ b/tests/wpt/meta/css/css-values/vh_not_refreshing_on_chrome.html.ini @@ -0,0 +1,2 @@ +[vh_not_refreshing_on_chrome.html] + expected: FAIL diff --git a/tests/wpt/meta/css/cssom-view/idlharness.html.ini b/tests/wpt/meta/css/cssom-view/idlharness.html.ini index 30bf0819a62..2ce851386f3 100644 --- a/tests/wpt/meta/css/cssom-view/idlharness.html.ini +++ b/tests/wpt/meta/css/cssom-view/idlharness.html.ini @@ -604,3 +604,12 @@ [Document interface: calling caretPositionFromPoint(double, double, ShadowRoot...) on document with too few arguments must throw TypeError] expected: FAIL + + [Document interface: operation caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)] + expected: FAIL + + [Document interface: document must inherit property "caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions)" with the proper type] + expected: FAIL + + [Document interface: calling caretPositionFromPoint(double, double, optional CaretPositionFromPointOptions) on document with too few arguments must throw TypeError] + expected: FAIL diff --git a/tests/wpt/meta/css/cssom-view/smooth-scroll-nonstop.html.ini b/tests/wpt/meta/css/cssom-view/smooth-scroll-nonstop.html.ini new file mode 100644 index 00000000000..fb43d2b8fa1 --- /dev/null +++ b/tests/wpt/meta/css/cssom-view/smooth-scroll-nonstop.html.ini @@ -0,0 +1,4 @@ +[smooth-scroll-nonstop.html] + expected: TIMEOUT + [noop scrollIntoView doesn't interrupt ongoing smooth scroll.] + expected: TIMEOUT diff --git a/tests/wpt/meta/dom/events/EventTarget-constructible.any.js.ini b/tests/wpt/meta/dom/events/EventTarget-constructible.any.js.ini new file mode 100644 index 00000000000..dd3d602fbb0 --- /dev/null +++ b/tests/wpt/meta/dom/events/EventTarget-constructible.any.js.ini @@ -0,0 +1,8 @@ +[EventTarget-constructible.any.worker.html] + [A constructed EventTarget implements dispatch correctly] + expected: FAIL + + +[EventTarget-constructible.any.html] + [A constructed EventTarget implements dispatch correctly] + expected: FAIL diff --git a/tests/wpt/meta/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini b/tests/wpt/meta/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini index 31690f3ffdc..3292b652dbf 100644 --- a/tests/wpt/meta/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini +++ b/tests/wpt/meta/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html.ini @@ -28,3 +28,6 @@ [Basic AttributePart cloning with values] expected: FAIL + + [Basic AttributePart parsing with multiple attributes] + expected: FAIL diff --git a/tests/wpt/meta/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini b/tests/wpt/meta/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini deleted file mode 100644 index 3982572f016..00000000000 --- a/tests/wpt/meta/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[basic-dom-part-declarative-pi-syntax.tentative.html] - [Basic declarative DOM Parts (Main Document)] - expected: FAIL - - [Basic declarative DOM Parts (Template)] - expected: FAIL - - [Basic declarative DOM Parts (Clone)] - expected: FAIL - - [Basic declarative DOM Parts (PartClone)] - expected: FAIL diff --git a/tests/wpt/meta/fetch/api/basic/keepalive.any.js.ini b/tests/wpt/meta/fetch/api/basic/keepalive.any.js.ini index 9aa4b06752c..2ce2d784ab5 100644 --- a/tests/wpt/meta/fetch/api/basic/keepalive.any.js.ini +++ b/tests/wpt/meta/fetch/api/basic/keepalive.any.js.ini @@ -1,19 +1,15 @@ [keepalive.any.html] - expected: TIMEOUT [keepalive in onunload in nested frame in another window] expected: FAIL - [[keepalive\] simple GET request on 'pagehide' [no payload\]; setting up] - expected: TIMEOUT - - [[keepalive\] simple GET request on 'unload' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple GET request on 'unload' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'load' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple GET request on 'pagehide' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'pagehide' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple POST request on 'unload' [no payload\]] + expected: FAIL - [[keepalive\] simple POST request on 'unload' [no payload\]; setting up] - expected: NOTRUN + [[keepalive\] simple POST request on 'pagehide' [no payload\]] + expected: FAIL diff --git a/tests/wpt/meta/fetch/metadata/generated/element-img-environment-change.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/element-img-environment-change.sub.html.ini index 4648085f98d..5e6dca448af 100644 --- a/tests/wpt/meta/fetch/metadata/generated/element-img-environment-change.sub.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/element-img-environment-change.sub.html.ini @@ -1,7 +1,7 @@ [element-img-environment-change.sub.html] expected: TIMEOUT [sec-fetch-site - Not sent to non-trustworthy same-site destination, no attributes] - expected: TIMEOUT + expected: FAIL [sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes] expected: NOTRUN diff --git a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js.ini b/tests/wpt/meta/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/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/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/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini new file mode 100644 index 00000000000..18cea856e89 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.html] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini new file mode 100644 index 00000000000..4fa5266a456 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.shadow.html] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini new file mode 100644 index 00000000000..c7203b17045 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.shadow.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini new file mode 100644 index 00000000000..61941d9bc47 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini new file mode 100644 index 00000000000..c95e1c74724 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.with-render-states-and-filter.html] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini new file mode 100644 index 00000000000..8cbdaa1ebe8 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html.ini @@ -0,0 +1,2 @@ +[2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html] + expected: TIMEOUT diff --git a/tests/wpt/meta/html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html.ini b/tests/wpt/meta/html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html.ini index fdc27d37788..830aeb8ae48 100644 --- a/tests/wpt/meta/html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html.ini +++ b/tests/wpt/meta/html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html.ini @@ -1,6 +1,7 @@ [autofocus-dialog.html] + expected: TIMEOUT [<dialog> can contain autofocus, without stopping page autofocus content from working] expected: FAIL [<dialog>-contained autofocus element gets focused when the dialog is shown] - expected: FAIL + expected: TIMEOUT diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini index 68203d2a082..26704422bbe 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini +++ b/tests/wpt/meta/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: CRASH + expected: TIMEOUT [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/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-1.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-1.html.ini index bbc1f35d8d9..5799e6c26cd 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-1.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-1.html.ini @@ -1,3 +1,3 @@ [iframe_sandbox_popups_nonescaping-1.html] [Check that popups from a sandboxed iframe do not escape the sandbox] - expected: FAIL + expected: NOTRUN 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 90c8bd71ded..1ae1c2cc134 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: CRASH [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 ff6467094b8..ccdaf8d61b2 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,3 +1,3 @@ [iframe_sandbox_popups_nonescaping-3.html] [Check that popups from a sandboxed iframe do not escape the sandbox] - expected: FAIL + expected: NOTRUN diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/sandbox-top-navigation-cross-site.tentative.sub.window.js.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/sandbox-top-navigation-cross-site.tentative.sub.window.js.ini index 8af95703e25..dd7548a86bc 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/sandbox-top-navigation-cross-site.tentative.sub.window.js.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/sandbox-top-navigation-cross-site.tentative.sub.window.js.ini @@ -1,7 +1,6 @@ [sandbox-top-navigation-cross-site.tentative.sub.window.html] - expected: TIMEOUT [A cross-site unsandboxed iframe navigation consumes user activation and disallows top-level navigation.] - expected: TIMEOUT + expected: FAIL [A same-site unsandboxed iframe navigation does not consume user activation and allows top-level navigation.] expected: FAIL diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/non-active-document.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/non-active-document.html.ini new file mode 100644 index 00000000000..3cdeb8ebcbc --- /dev/null +++ b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/non-active-document.html.ini @@ -0,0 +1,3 @@ +[non-active-document.html] + [DOMParser] + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini b/tests/wpt/meta/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini new file mode 100644 index 00000000000..05aa7ddc17f --- /dev/null +++ b/tests/wpt/meta/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html.ini @@ -0,0 +1,3 @@ +[option-computed-style.tentative.html] + [appearance:base-select options should have a checkmark with empty alt text.] + expected: FAIL diff --git a/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/document-write/module-delayed.html.ini b/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/document-write/module-delayed.html.ini deleted file mode 100644 index fc0233f5241..00000000000 --- a/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/document-write/module-delayed.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[module-delayed.html] - [async document.write in a module] - expected: FAIL diff --git a/tests/wpt/meta/mediasession/idlharness.window.js.ini b/tests/wpt/meta/mediasession/idlharness.window.js.ini index d0edde1c114..d0631b56c8b 100644 --- a/tests/wpt/meta/mediasession/idlharness.window.js.ini +++ b/tests/wpt/meta/mediasession/idlharness.window.js.ini @@ -55,3 +55,12 @@ [ChapterInformation interface: attribute artwork] expected: FAIL + + [MediaSession interface: operation setScreenshareActive(boolean)] + expected: FAIL + + [MediaSession interface: navigator.mediaSession must inherit property "setScreenshareActive(boolean)" with the proper type] + expected: FAIL + + [MediaSession interface: calling setScreenshareActive(boolean) on navigator.mediaSession with too few arguments must throw TypeError] + expected: FAIL diff --git a/tests/wpt/meta/resource-timing/content-type.html.ini b/tests/wpt/meta/resource-timing/content-type.html.ini index d1f4eb3ab5d..ec8afaefc79 100644 --- a/tests/wpt/meta/resource-timing/content-type.html.ini +++ b/tests/wpt/meta/resource-timing/content-type.html.ini @@ -80,3 +80,6 @@ [This test validates the content-type of resources. 26] expected: NOTRUN + + [This test validates the content-type of resources. 27] + expected: NOTRUN diff --git a/tests/wpt/meta/url/a-element-xhtml.xhtml.ini b/tests/wpt/meta/url/a-element-xhtml.xhtml.ini index 075175a09e2..6cee51b6da3 100644 --- a/tests/wpt/meta/url/a-element-xhtml.xhtml.ini +++ b/tests/wpt/meta/url/a-element-xhtml.xhtml.ini @@ -543,6 +543,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> against <about:blank>] + expected: FAIL + [a-element-xhtml.xhtml?exclude=(file|javascript|mailto)] [Parsing: <///test> against <http://example.org/>] diff --git a/tests/wpt/meta/url/a-element.html.ini b/tests/wpt/meta/url/a-element.html.ini index 0e460dd840a..1f1c2631fce 100644 --- a/tests/wpt/meta/url/a-element.html.ini +++ b/tests/wpt/meta/url/a-element.html.ini @@ -543,6 +543,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> against <about:blank>] + expected: FAIL + [a-element.html?include=javascript] diff --git a/tests/wpt/meta/url/url-constructor.any.js.ini b/tests/wpt/meta/url/url-constructor.any.js.ini index 673218d9aa6..019089582f8 100644 --- a/tests/wpt/meta/url/url-constructor.any.js.ini +++ b/tests/wpt/meta/url/url-constructor.any.js.ini @@ -1063,6 +1063,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.worker.html?exclude=(file|javascript|mailto)] [Parsing: </> against <file://h/C:/a/b>] @@ -1263,6 +1266,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.html?include=file] [Parsing: </> against <file://h/C:/a/b>] @@ -1424,6 +1430,9 @@ [Parsing: </\\//\\/a/../> against <file:///>] expected: FAIL + [Parsing: <file:///w|/m> without base] + expected: FAIL + [url-constructor.any.html?include=javascript] diff --git a/tests/wpt/meta/webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.tentative.html.ini b/tests/wpt/meta/webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.tentative.html.ini new file mode 100644 index 00000000000..aa6c9e5b826 --- /dev/null +++ b/tests/wpt/meta/webstorage/localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.tentative.html.ini @@ -0,0 +1,4 @@ +[localstorage-about-blank-3P-iframe-opens-3P-window.partitioned.tentative.html] + expected: TIMEOUT + [StorageKey: test 3P about:blank window opened from a 3P iframe] + expected: TIMEOUT diff --git a/tests/wpt/tests/.azure-pipelines.yml b/tests/wpt/tests/.azure-pipelines.yml index c018c592d1d..2455ad8df12 100644 --- a/tests/wpt/tests/.azure-pipelines.yml +++ b/tests/wpt/tests/.azure-pipelines.yml @@ -35,7 +35,7 @@ jobs: displayName: 'affected tests: Safari Technology Preview' condition: eq(variables['Build.Reason'], 'PullRequest') pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -52,7 +52,7 @@ jobs: displayName: 'affected tests without changes: Safari Technology Preview' condition: eq(variables['Build.Reason'], 'PullRequest') pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -94,7 +94,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_infrastructure'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -125,7 +125,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -141,7 +141,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -157,7 +157,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -173,7 +173,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -189,7 +189,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: # full checkout required - task: UsePythonVersion@0 @@ -209,7 +209,7 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: # full checkout required - task: UsePythonVersion@0 @@ -455,7 +455,7 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -495,7 +495,7 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: @@ -532,7 +532,7 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-13' + vmImage: 'macOS-14' steps: - task: UsePythonVersion@0 inputs: diff --git a/tests/wpt/tests/.github/workflows/docker.yml b/tests/wpt/tests/.github/workflows/docker.yml index 655e881869d..a55d0f626d2 100644 --- a/tests/wpt/tests/.github/workflows/docker.yml +++ b/tests/wpt/tests/.github/workflows/docker.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 # Based on https://docs.github.com/en/actions/publishing-packages/publishing-docker-images. - name: Log in to the Container registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -40,7 +40,7 @@ jobs: latest type=raw,value=${{ inputs.tag }} - name: Build and push the Docker image - uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5.4.0 with: context: ./tools/docker push: true diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index.any.js b/tests/wpt/tests/IndexedDB/idbcursor_advance_index.any.js new file mode 100644 index 00000000000..c928bdabd5f --- /dev/null +++ b/tests/wpt/tests/IndexedDB/idbcursor_advance_index.any.js @@ -0,0 +1,258 @@ +// META: global=window,worker +// META: title=IDBCursor.advance() +// META: script=resources/support.js +// @author Microsoft <https://www.microsoft.com> +// @author Odin Hørthe Omdal <mailto:odinho@opera.com> +// @author Intel <http://www.intel.com> + +'use strict'; + +function createObjectStoreWithIndexAndPopulate(db, records) { + let objStore = db.createObjectStore("test", { keyPath: "pKey" }); + objStore.createIndex("index", "iKey"); + for (let i = 0; i < records.length; i++) { + objStore.add(records[i]); + } + return objStore; +} + +function setOnUpgradeNeeded(dbObj, records) { + return function (event) { + dbObj.db = event.target.result; + createObjectStoreWithIndexAndPopulate(dbObj.db, records); + }; +} + +async_test(t => { + let dbObj = {}; + let count = 0; + const records = [{ pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" }, + { pKey: "primaryKey_2", iKey: "indexKey_2" }, + { pKey: "primaryKey_3", iKey: "indexKey_3" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = setOnUpgradeNeeded(dbObj, records); + + open_rq.onsuccess = function (e) { + let cursor_rq = dbObj.db.transaction("test", "readonly", { durability: 'relaxed' }) + .objectStore("test") + .index("index") + .openCursor(); + + cursor_rq.onsuccess = t.step_func(function (e) { + let cursor = e.target.result; + assert_true(cursor instanceof IDBCursor); + + switch (count) { + case 0: + count += 3; + cursor.advance(3); + break; + case 3: + let record = cursor.value; + assert_equals(record.pKey, records[count].pKey, "record.pKey"); + assert_equals(record.iKey, records[count].iKey, "record.iKey"); + t.done(); + break; + default: + assert_unreached("unexpected count"); + break; + } + }); + }; +}, "index - iterate cursor number of times specified by count"); + +async_test(t => { + let dbObj = {}; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = setOnUpgradeNeeded(dbObj, records); + + open_rq.onsuccess = function (e) { + let cursor_rq = dbObj.db.transaction("test", "readonly", { durability: 'relaxed' }) + .objectStore("test") + .index("index") + .openCursor(); + + cursor_rq.onsuccess = t.step_func(function (e) { + let cursor = e.target.result; + + assert_true(cursor != null, "cursor exist"); + assert_throws_js(TypeError, + function () { cursor.advance(-1); }); + + t.done(); + }); + }; +}, "attempt to pass a count parameter that is not a number"); + +async_test(t => { + let dbObj = {}; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = setOnUpgradeNeeded(dbObj, records); + + open_rq.onsuccess = function (e) { + let cursor_rq = dbObj.db.transaction("test", "readonly", { durability: 'relaxed' }) + .objectStore("test") + .index("index") + .openCursor(undefined, "next"); + + cursor_rq.onsuccess = t.step_func(function (e) { + let cursor = e.target.result; + + assert_true(cursor != null, "cursor exist"); + assert_throws_js(TypeError, + function () { cursor.advance(-1); }); + + t.done(); + }); + }; +}, "index - attempt to advance backwards"); + +async_test(t => { + let dbObj = {}; + let count = 0; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" }, + { pKey: "primaryKey_1-2", iKey: "indexKey_1" } + ]; + const expected = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1-2", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = setOnUpgradeNeeded(dbObj, records); + + open_rq.onsuccess = function (e) { + let cursor_rq = dbObj.db.transaction("test", "readonly", { durability: 'relaxed' }) + .objectStore("test") + .index("index") + .openCursor(); + + cursor_rq.onsuccess = t.step_func(function (e) { + let cursor = e.target.result; + if (!cursor) { + assert_equals(count, expected.length, "cursor run count") + t.done() + } + + let record = cursor.value; + assert_equals(record.pKey, expected[count].pKey, "primary key"); + assert_equals(record.iKey, expected[count].iKey, "index key"); + + cursor.advance(2); + count++; + }); + }; +}, "index - iterate to the next record"); + +async_test(t => { + let db; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = function (event) { + db = event.target.result; + let objStore = createObjectStoreWithIndexAndPopulate(db, records); + let rq = objStore.index("index").openCursor(); + rq.onsuccess = t.step_func(function (event) { + let cursor = event.target.result; + assert_true(cursor instanceof IDBCursor); + assert_throws_js(TypeError, + function () { cursor.advance(0); }); + + t.done(); + }); + } +}, "Calling advance() with count argument 0 should throw TypeError."); + +async_test(t => { + let db; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = function (event) { + db = event.target.result; + let objStore = createObjectStoreWithIndexAndPopulate(db, records); + let rq = objStore.index("index").openCursor(); + rq.onsuccess = t.step_func(function (event) { + let cursor = event.target.result; + assert_true(cursor instanceof IDBCursor); + + event.target.transaction.abort(); + assert_throws_dom("TransactionInactiveError", + function () { cursor.advance(1); }); + + t.done(); + }); + } +}, "Calling advance() should throws an exception TransactionInactiveError when the transaction is not active."); + +async_test(t => { + let db; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = function (event) { + db = event.target.result; + let objStore = createObjectStoreWithIndexAndPopulate(db, records); + let rq = objStore.index("index").openCursor(); + rq.onsuccess = t.step_func(function (event) { + let cursor = event.target.result; + assert_true(cursor instanceof IDBCursor); + + cursor.advance(1); + assert_throws_dom("InvalidStateError", + function () { cursor.advance(1); }); + + t.done(); + }); + } +}, "Calling advance() should throw DOMException when the cursor is currently being iterated."); + +async_test(t => { + let db; + const records = [ + { pKey: "primaryKey_0", iKey: "indexKey_0" }, + { pKey: "primaryKey_1", iKey: "indexKey_1" } + ]; + + let open_rq = createdb(t); + open_rq.onupgradeneeded = function (event) { + db = event.target.result; + let objStore = createObjectStoreWithIndexAndPopulate(db, records); + let rq = objStore.index("index").openCursor(); + rq.onsuccess = t.step_func(function (event) { + let cursor = event.target.result; + assert_true(cursor instanceof IDBCursor, "cursor exist"); + + db.deleteObjectStore("test"); + assert_throws_dom("InvalidStateError", + function () { cursor.advance(1); }); + + t.done(); + }); + } +}, "If the cursor's source or effective object store has been deleted, the implementation MUST throw a DOMException of type InvalidStateError"); diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index.htm deleted file mode 100644 index 4c0c0b697a3..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index.htm +++ /dev/null @@ -1,57 +0,0 @@ -<!DOCTYPE html> -<title>IDBCursor.advance() - index - iterate cursor number of times specified by count </title> -<link rel="author" title="Microsoft" href="http://www.microsoft.com"> -<script src=/resources/testharness.js></script> -<script src=/resources/testharnessreport.js></script> -<script src=resources/support.js></script> - -<script> - var db, - count = 0, - t = async_test(), - records = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }, - { pKey: "primaryKey_2", iKey: "indexKey_2" }, - { pKey: "primaryKey_3", iKey: "indexKey_3" }]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function(e) { - db = e.target.result; - var store = db.createObjectStore("test", {keyPath:"pKey"}); - store.createIndex("idx", "iKey"); - - for(var i = 0; i < records.length; i++) { - store.add(records[i]); - } - }; - - open_rq.onsuccess = function (e) { - var cursor_rq = db.transaction("test", "readonly", {durability: 'relaxed'}) - .objectStore("test") - .index("idx") - .openCursor(); - - cursor_rq.onsuccess = t.step_func(function(e) { - var cursor = e.target.result; - assert_true(cursor instanceof IDBCursor); - - switch(count) { - case 0: - count += 3; - cursor.advance(3); - break; - case 3: - var record = cursor.value; - assert_equals(record.pKey, records[count].pKey, "record.pKey"); - assert_equals(record.iKey, records[count].iKey, "record.iKey"); - t.done(); - break; - default: - assert_unreached("unexpected count"); - break; - } - }); - } -</script> - -<div id=log></div> diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index2.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index2.htm deleted file mode 100644 index 04842f71c31..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index2.htm +++ /dev/null @@ -1,47 +0,0 @@ -<!DOCTYPE html> -<title>IDBCursor.advance() - attempt to pass a count parameter that is not a number</title> -<link rel="author" href="mailto:odinho@opera.com" title="Odin Hørthe Omdal"> -<link rel=help href="http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<link rel=assert title="The value passed into the count parameter was zero or a negative number."> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> - -<script> - - var db, - t = async_test(), - records = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" } ]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function(e) { - db = e.target.result; - var objStore = db.createObjectStore("test", {keyPath:"pKey"}); - - objStore.createIndex("index", "iKey"); - - for(var i = 0; i < records.length; i++) - objStore.add(records[i]); - }; - - open_rq.onsuccess = function(e) { - var cursor_rq = db.transaction("test", "readonly", {durability: 'relaxed'}) - .objectStore("test") - .index("index") - .openCursor(); - - cursor_rq.onsuccess = t.step_func(function(e) { - var cursor = e.target.result; - - assert_true(cursor != null, "cursor exist"); - assert_throws_js(TypeError, - function() { cursor.advance(document); }); - - t.done(); - }); - }; - -</script> - -<div id="log"></div> diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index3.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index3.htm deleted file mode 100644 index a797286e7c1..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index3.htm +++ /dev/null @@ -1,47 +0,0 @@ -<!DOCTYPE html> -<title>IDBCursor.advance() - index - attempt to advance backwards</title> -<link rel="author" href="mailto:odinho@opera.com" title="Odin Hørthe Omdal"> -<link rel=help href="http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<link rel=assert title="The value passed into the count parameter was zero or a negative number."> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> - -<script type="text/javascript"> - - var db, - t = async_test(), - records = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" } ]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function(e) { - db = e.target.result; - var objStore = db.createObjectStore("test", { keyPath:"pKey" }); - - objStore.createIndex("index", "iKey"); - - for (var i = 0; i < records.length; i++) - objStore.add(records[i]); - }; - - open_rq.onsuccess = function(e) { - var cursor_rq = db.transaction("test", "readonly", {durability: 'relaxed'}) - .objectStore("test") - .index("index") - .openCursor(undefined, "next"); - - cursor_rq.onsuccess = t.step_func(function(e) { - var cursor = e.target.result; - - assert_true(cursor != null, "cursor exist"); - assert_throws_js(TypeError, - function() { cursor.advance(-1); }); - - t.done(); - }); - }; - -</script> - -<div id="log"></div> diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index5.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index5.htm deleted file mode 100644 index 5e60aefd5a5..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index5.htm +++ /dev/null @@ -1,55 +0,0 @@ -<!DOCTYPE html> -<title>IDBCursor.advance() - index - iterate to the next record</title> -<link rel="author" href="mailto:odinho@opera.com" title="Odin Hørthe Omdal"> -<link rel=help href="http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<link rel=assert title="The operation runs the steps for iterating a cursor count number of times with null as key and this cursor as cursor."> -<link rel=assert title="The number of advances forward the cursor should make."> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> - -<script> - var db, - count = 0, - t = async_test(), - records = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }, - { pKey: "primaryKey_1-2", iKey: "indexKey_1" } ], - expected = [ { pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1-2", iKey: "indexKey_1" } ]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function(e) { - db = e.target.result - var objStore = db.createObjectStore("test", { keyPath:"pKey" }) - - objStore.createIndex("index", "iKey") - - for (var i = 0; i < records.length; i++) - objStore.add(records[i]) - }; - - open_rq.onsuccess = function(e) { - var cursor_rq = db.transaction("test", "readonly", {durability: 'relaxed'}) - .objectStore("test") - .index("index") - .openCursor(); - - cursor_rq.onsuccess = t.step_func(function(e) { - var cursor = e.target.result; - if (!cursor) { - assert_equals(count, expected.length, "cursor run count") - t.done() - } - - var record = cursor.value; - assert_equals(record.pKey, expected[count].pKey, "primary key"); - assert_equals(record.iKey, expected[count].iKey, "index key"); - - cursor.advance(2); - count++; - }); - }; -</script> - -<div id="log"></div> diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index6.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index6.htm deleted file mode 100644 index 3a1168f57a5..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index6.htm +++ /dev/null @@ -1,37 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8"> -<title>IDBCursor.advance() - index - throw TypeError</title> -<link rel="author" title="Intel" href="http://www.intel.com"> -<link rel="help" href="https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> -<div id="log"></div> -<script> - var db, - t = async_test(), - records = [{ pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function (event) { - db = event.target.result; - var objStore = db.createObjectStore("store", {keyPath : "pKey"}); - objStore.createIndex("index", "iKey"); - for (var i = 0; i < records.length; i++) { - objStore.add(records[i]); - } - var rq = objStore.index("index").openCursor(); - rq.onsuccess = t.step_func(function(event) { - var cursor = event.target.result; - assert_true(cursor instanceof IDBCursor); - - assert_throws_js(TypeError, function() { - cursor.advance(0); - }, "Calling advance() with count argument 0 should throw TypeError."); - - t.done(); - }); - } -</script> - diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index7.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index7.htm deleted file mode 100644 index f5ecc6144c2..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index7.htm +++ /dev/null @@ -1,39 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8"> -<title>IDBCursor.advance() - index - throw TransactionInactiveError</title> -<link rel="author" title="Intel" href="http://www.intel.com"> -<link rel="help" href="https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> -<div id="log"></div> -<script> - var db, - t = async_test(), - records = [{ pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function (event) { - db = event.target.result; - var objStore = db.createObjectStore("store", {keyPath : "pKey"}); - objStore.createIndex("index", "iKey"); - for (var i = 0; i < records.length; i++) { - objStore.add(records[i]); - } - var rq = objStore.index("index").openCursor(); - rq.onsuccess = t.step_func(function(event) { - var cursor = event.target.result; - assert_true(cursor instanceof IDBCursor); - - event.target.transaction.abort(); - assert_throws_dom("TransactionInactiveError", function() { - cursor.advance(1); - }, "Calling advance() should throws an exception TransactionInactiveError when the transaction is not active."); - - t.done(); - }); - } - -</script> - diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index8.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index8.htm deleted file mode 100644 index 1f9ff9c453a..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index8.htm +++ /dev/null @@ -1,38 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8"> -<title>IDBCursor.advance() - index - throw InvalidStateError</title> -<link rel="author" title="Intel" href="http://www.intel.com"> -<link rel="help" href="https://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#widl-IDBCursor-advance-void-unsigned-long-count"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> -<div id="log"></div> -<script> - var db, - t = async_test(), - records = [{ pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function (event) { - db = event.target.result; - var objStore = db.createObjectStore("store", {keyPath : "pKey"}); - objStore.createIndex("index", "iKey"); - for (var i = 0; i < records.length; i++) { - objStore.add(records[i]); - } - var rq = objStore.index("index").openCursor(); - rq.onsuccess = t.step_func(function(event) { - var cursor = event.target.result; - assert_true(cursor instanceof IDBCursor); - - cursor.advance(1); - assert_throws_dom("InvalidStateError", function() { - cursor.advance(1); - }, "Calling advance() should throw DOMException when the cursor is currently being iterated."); - - t.done(); - }); - } -</script> - diff --git a/tests/wpt/tests/IndexedDB/idbcursor_advance_index9.htm b/tests/wpt/tests/IndexedDB/idbcursor_advance_index9.htm deleted file mode 100644 index 1756419c6d2..00000000000 --- a/tests/wpt/tests/IndexedDB/idbcursor_advance_index9.htm +++ /dev/null @@ -1,37 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8"> -<title>IDBCursor.advance() - index - throw InvalidStateError caused by object store been deleted</title> -<link rel="author" title="Intel" href="http://www.intel.com"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="resources/support.js"></script> -<div id="log"></div> -<script> - var db, - t = async_test(), - records = [{ pKey: "primaryKey_0", iKey: "indexKey_0" }, - { pKey: "primaryKey_1", iKey: "indexKey_1" }]; - - var open_rq = createdb(t); - open_rq.onupgradeneeded = function (event) { - db = event.target.result; - var objStore = db.createObjectStore("store", {keyPath : "pKey"}); - objStore.createIndex("index", "iKey"); - for (var i = 0; i < records.length; i++) { - objStore.add(records[i]); - } - var rq = objStore.index("index").openCursor(); - rq.onsuccess = t.step_func(function(event) { - var cursor = event.target.result; - assert_true(cursor instanceof IDBCursor, "cursor exist"); - - db.deleteObjectStore("store"); - assert_throws_dom("InvalidStateError", function() { - cursor.advance(1); - }, "If the cursor's source or effective object store has been deleted, the implementation MUST throw a DOMException of type InvalidStateError"); - - t.done(); - }); - } -</script> - diff --git a/tests/wpt/tests/IndexedDB/idbobjectstore_getAll.any.js b/tests/wpt/tests/IndexedDB/idbobjectstore_getAll.any.js index db7098f71e8..600d0007889 100644 --- a/tests/wpt/tests/IndexedDB/idbobjectstore_getAll.any.js +++ b/tests/wpt/tests/IndexedDB/idbobjectstore_getAll.any.js @@ -146,3 +146,18 @@ getall_test((t, connection) => { t.done(); }); }, 'zero maxCount'); + +getall_test((t, connection) => { + const transaction = connection.transaction('out-of-line', 'readonly'); + const store = transaction.objectStore('out-of-line'); + const req = store.getAll(); + transaction.commit(); + transaction.oncomplete = + t.unreached_func('transaction completed before request succeeded'); + + req.onerror = t.unreached_func('getAll request should succeed'); + req.onsuccess = t.step_func(evt => { + assert_array_equals(evt.target.result, alphabet.map(c => `value-${c}`)); + transaction.oncomplete = t.step_func_done(); + }); +}, 'Get all values with transaction.commit()'); diff --git a/tests/wpt/tests/clipboard-apis/async-svg-read-write.tentative.https.html b/tests/wpt/tests/clipboard-apis/async-svg-read-write.tentative.https.html new file mode 100644 index 00000000000..78c814a1f1a --- /dev/null +++ b/tests/wpt/tests/clipboard-apis/async-svg-read-write.tentative.https.html @@ -0,0 +1,76 @@ +<!doctype html> +<meta charset="utf-8"> +<title> + Async Clipboard write ([image/svg+xml ClipboardItem]) -> read and write svg tests +</title> +<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api"> +<body>Body needed for test_driver.click()</body> +<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/user-activation.js"></script> +<script> +'use strict'; +// This function removes extra spaces between tags in svg. For example, the +// following svg: "<svg> <g> </g> </svg>" would turn into this +// svg: "<svg> <g> </g> </svg>" +// We remove the extra spaces because in svg they are considered equivalent, +// but when we are comparing for equality the spaces make a difference. +function reformatSvg(svgString) { + return svgString.replace(/\>\s*\</g, '> <'); +} + +async function readWriteTest(textInput) { + await test_driver.set_permission({name: 'clipboard-read'}, 'granted'); + await test_driver.set_permission({name: 'clipboard-write'}, 'granted'); + const blobInput = new Blob([textInput.input], {type: 'image/svg+xml'}); + const clipboardItem = new ClipboardItem({'image/svg+xml': blobInput}); + await waitForUserActivation(); + await navigator.clipboard.write([clipboardItem]); + await waitForUserActivation(); + const clipboardItems = + await navigator.clipboard.read({type: 'image/svg+xml'}); + + const svg = clipboardItems[0]; + assert_equals(svg.types.length, 1); + assert_equals(svg.types[0], 'image/svg+xml'); + + const blobOutput = await svg.getType('image/svg+xml'); + assert_equals(blobOutput.type, 'image/svg+xml'); + + const blobText = await (new Response(blobOutput)).text(); + const outputSvg = reformatSvg(blobText); + if ("expected" in textInput) { + assert_equals(outputSvg, reformatSvg(textInput.expected)); + } else { + assert_equals(outputSvg, reformatSvg(textInput.input)); + } +} +const testCases = [ + { input: '<svg></svg>' }, + { input: '<svg> <circle cx="50" cy="50" r="40"/> </svg>', + expected: '<svg> <circle cx="50" cy="50" r="40"> </circle> </svg>' }, + { input: '<svg> <script>const a = 5;\<\/script> <a href="javascript:alert(2)"> test </a> </svg>', + expected: '<svg> <a> test </a> </svg>' }, + { input: '<svg> <style>.foo {fill: green;}</style>' + + '<circle class=\"foo\" cx=\"50\" cy=\"50\" r=\"40\" /> </svg>', + // The extra style attributes is due to read-time sanitization. + // We would like to improve the expectation but need a better sanitizer first. + expected: '<svg style=\"color: rgb(0, 0, 0); font-size: medium;' + + ' font-style: normal; font-variant-ligatures: normal;' + + ' font-variant-caps: normal; font-weight: 400;' + + ' letter-spacing: normal; orphans: 2; text-align: start;' + + ' text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px;' + + ' -webkit-text-stroke-width: 0px; white-space: normal;' + + ' text-decoration-thickness: initial; text-decoration-style: initial;' + + ' text-decoration-color: initial;\"> <circle class=\"foo\" cx=\"50\"' + + ' cy=\"50\" r=\"40\"> </circle> </svg>' }, +]; + +promise_test(async t => { + for (const testCase of testCases) { + await readWriteTest(testCase); + } +}, 'Verify read and write of some image/svg+xml content'); +</script> diff --git a/tests/wpt/tests/cookies/attributes/secure.https.html b/tests/wpt/tests/cookies/attributes/secure.https.html index 93088996943..4557cd248fb 100644 --- a/tests/wpt/tests/cookies/attributes/secure.https.html +++ b/tests/wpt/tests/cookies/attributes/secure.https.html @@ -54,7 +54,12 @@ cookie: "test=8; Secure ;", expected: "test=8", name: "Set cookie for space Secure with ;", - } + }, + { + cookie: ["testa9a=x; secure; ", "; secure", "testa9b=y; secure;"], + expected: "testa9a=x; testa9b=y", + name: "Ignore name- and value-less `Set-Cookie: ; secure`", + }, ]; for (const test of secureTests) { diff --git a/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account-button-flow.tentative.https.html b/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account-button-flow.tentative.https.html index 996523af847..3b90782713f 100644 --- a/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account-button-flow.tentative.https.html +++ b/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account-button-flow.tentative.https.html @@ -46,7 +46,8 @@ fedcm_test(async t => { type = await fedcm_get_dialog_type_promise(t); assert_equals(type, "AccountChooser"); - await window.test_driver.select_fedcm_account(1); + // Select the first account, which is the most recently signed in account. + await window.test_driver.select_fedcm_account(0); const cred = await cred_promise; assert_equals(cred.token, "account_id=jane_doe"); }, 'Test that the "Use Other Account" button works correctly.'); diff --git a/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account.tentative.https.html b/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account.tentative.https.html index 2022bbc0f72..96006cce68c 100644 --- a/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account.tentative.https.html +++ b/tests/wpt/tests/credential-management/fedcm-button-and-other-account/fedcm-use-other-account.tentative.https.html @@ -41,7 +41,8 @@ fedcm_test(async t => { type = await fedcm_get_dialog_type_promise(t); assert_equals(type, "AccountChooser"); - await window.test_driver.select_fedcm_account(1); + // Select the first account, which is the most recently signed in account. + await window.test_driver.select_fedcm_account(0); const cred = await cred_promise; assert_equals(cred.token, "account_id=jane_doe"); }, 'Test that the "Use Other Account" button works correctly.'); diff --git a/tests/wpt/tests/css/css-anchor-position/inline-grid-try-options-crash.html b/tests/wpt/tests/css/css-anchor-position/inline-grid-try-options-crash.html new file mode 100644 index 00000000000..712f7f1d481 --- /dev/null +++ b/tests/wpt/tests/css/css-anchor-position/inline-grid-try-options-crash.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html class=test-wait> + <title>CSS Anchor Positioning: Don't crash when animating position and position-try-options in grid</title> + <link rel="help" href="https://crbug.com/339686368"> + <style> + @keyframes --anim { + from { + position: absolute; + position-try-options: flip-block; + } + } + #parent { + display: inline-grid; + } + #victim { + height:100em; + } + #victim.anim { + animation: --anim 100ms linear; + } + </style> + <p>PASS if no crash</p> + <div id=parent> + <div id=victim></div> + </div> + <script> + victim.classList.add('anim'); + victim.addEventListener('animationend', () => { + victim.offsetTop; + document.documentElement.classList.remove('test-wait'); + }); + </script> +</html>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-anchor-position/try-tactic-back-to-base.html b/tests/wpt/tests/css/css-anchor-position/try-tactic-back-to-base.html new file mode 100644 index 00000000000..9a11fc7fffc --- /dev/null +++ b/tests/wpt/tests/css/css-anchor-position/try-tactic-back-to-base.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<title>CSS Anchor Positioning: try-tactic, back to base values</title> +<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#typedef-position-try-options-try-tactic"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> + #cb { + position: absolute; + width: 400px; + height: 200px; + border: 1px solid black; + } + #target { + position: absolute; + width: 50px; + height: 50px; + /* Does not fit ... */ + left: 180px; + top: 190px; + /* ... and neither does this. */ + position-try-options: flip-block; + background-color: coral; + + } +</style> +<div id=cb> + <div id=target></div> +</div> +<script> + test(() => { + let cs = getComputedStyle(target); + assert_equals(cs.left, '180px'); + assert_equals(cs.top, '190px'); + }, 'Should use base values when nothing fits'); +</script> diff --git a/tests/wpt/tests/css/css-backgrounds/background-size/background-size-contain-svg-view.html b/tests/wpt/tests/css/css-backgrounds/background-size/background-size-contain-svg-view.html new file mode 100644 index 00000000000..0d69e27e3dc --- /dev/null +++ b/tests/wpt/tests/css/css-backgrounds/background-size/background-size-contain-svg-view.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>SVG <view>-based sprites in background-image, background-size: contain</title> +<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-size"> +<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html"> +<style> + #target { + width: 100px; + height: 100px; + background-image: url('support/sprite-view-no-viewbox.svg#rect'); + background-repeat: no-repeat; + background-position: 50%; + border: 25px solid green; + box-sizing: border-box; + background-origin: border-box; + background-size: contain; + } +</style> +<p>Test passes if there is a filled green square.</p> +<div id="target"></div> diff --git a/tests/wpt/tests/css/css-backgrounds/background-size/background-size-cover-svg-view.html b/tests/wpt/tests/css/css-backgrounds/background-size/background-size-cover-svg-view.html new file mode 100644 index 00000000000..dc792792476 --- /dev/null +++ b/tests/wpt/tests/css/css-backgrounds/background-size/background-size-cover-svg-view.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>SVG <view>-based sprites in background-image, background-size: cover</title> +<link rel="help" href="https://drafts.csswg.org/css-backgrounds/#background-size"> +<link rel="match" href="../../reference/ref-filled-green-100px-square-only.html"> +<style> + #target { + width: 100px; + height: 100px; + background-image: url('support/sprite-view-no-viewbox.svg#rect'); + background-repeat: no-repeat; + background-position: 50%; + background-size: cover; + } +</style> +<p>Test passes if there is a filled green square.</p> +<div id="target"></div> diff --git a/tests/wpt/tests/css/css-backgrounds/background-size/support/sprite-view-no-viewbox.svg b/tests/wpt/tests/css/css-backgrounds/background-size/support/sprite-view-no-viewbox.svg new file mode 100644 index 00000000000..c17f5cf5826 --- /dev/null +++ b/tests/wpt/tests/css/css-backgrounds/background-size/support/sprite-view-no-viewbox.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <symbol id="rect-shape" viewBox="0 0 16 8"> + <rect width="16" height="8" fill="red"/> + <rect x="4" width="8" height="8" fill="green"/> + </symbol> + <use href="#rect-shape"/> + <view id="rect" viewBox="0 0 16 8"/> +</svg> diff --git a/tests/wpt/tests/css/css-backgrounds/border-image-image-type-001.htm b/tests/wpt/tests/css/css-backgrounds/border-image-image-type-001.htm index 9486f0b8b6e..2c8d5941a4d 100644 --- a/tests/wpt/tests/css/css-backgrounds/border-image-image-type-001.htm +++ b/tests/wpt/tests/css/css-backgrounds/border-image-image-type-001.htm @@ -7,6 +7,7 @@ <link rel="match" href="reference/border-image-image-type-001-ref.html" /> <meta name="flags" content="svg" /> <meta name="assert" content="This test checks that the SVG image used for the border image is sliced into nine regions with inward offsets of: '40 30 20 10' vector coordinates from the top, right, bottom, and left edges of the image set by the 'border-image-slice' property." /> + <meta name="fuzzy" content="maxDifference=0-64; totalPixels=0-400"/> <style type="text/css"> div { diff --git a/tests/wpt/tests/css/css-contain/container-queries/container-for-shadow-dom.html b/tests/wpt/tests/css/css-contain/container-queries/container-for-shadow-dom.html index c3ae9612546..63d58f76982 100644 --- a/tests/wpt/tests/css/css-contain/container-queries/container-for-shadow-dom.html +++ b/tests/wpt/tests/css/css-contain/container-queries/container-for-shadow-dom.html @@ -6,23 +6,23 @@ <script src="/resources/testharnessreport.js"></script> <script src="support/cq-testcommon.js"></script> <style> - #inclusive-ancestor-across-root, - #inclusive-ancestor-skip-slotting, - #inclusive-ancestor-slotted, - #inclusive-ancestor-host, - #inclusive-ancestor-part, - #inclusive-ancestor-slotted-before, - #inclusive-ancestor-host-before, - #inclusive-ancestor-part-before, - #inclusive-ancestor-inner-part, - #inclusive-ancestor-slot-fallback, + #ancestor-across-root, + #ancestor-skip-slotting, + #ancestor-slotted, + #ancestor-host, + #ancestor-part, + #ancestor-slotted-before, + #ancestor-host-before, + #ancestor-part-before, + #ancestor-inner-part, + #ancestor-slot-fallback, #inner-scope-host-part { width: 400px; container-type: inline-size; } </style> -<div id="inclusive-ancestor-across-root"> +<div id="ancestor-across-root"> <div> <template shadowrootmode="open"> <style> @@ -35,7 +35,7 @@ </div> </div> -<div id="inclusive-ancestor-skip-slotting"> +<div id="ancestor-skip-slotting"> <div> <template shadowrootmode="open"> <style> @@ -57,7 +57,7 @@ </div> </div> -<div id="inclusive-ancestor-slotted"> +<div id="ancestor-slotted"> <div> <template shadowrootmode="open"> <style> @@ -76,7 +76,7 @@ </div> </div> -<div id="inclusive-ancestor-host"> +<div id="ancestor-host"> <div id="t4"> <template shadowrootmode="open"> <style> @@ -88,7 +88,7 @@ </div> </div> -<div id="inclusive-ancestor-part"> +<div id="ancestor-part"> <div> <template shadowrootmode="open"> <style> @@ -103,13 +103,13 @@ </template> <style> @container (width = 200px) { - #inclusive-ancestor-part > div::part(part) { color: green; } + #ancestor-part > div::part(part) { color: green; } } </style> </div> </div> -<div id="inclusive-ancestor-slotted-before"> +<div id="ancestor-slotted-before"> <div> <template shadowrootmode="open"> <style> @@ -137,7 +137,7 @@ </div> </div> -<div id="inclusive-ancestor-host-before"> +<div id="ancestor-host-before"> <div id="t7"> <template shadowrootmode="open"> <style> @@ -156,10 +156,10 @@ </div> </div> -<div id="inclusive-ancestor-part-before"> +<div id="ancestor-part-before"> <style> @container (width = 200px) { - #inclusive-ancestor-part-before > div::part(part)::before { + #ancestor-part-before > div::part(part)::before { content: "X"; color: green; } @@ -180,10 +180,10 @@ </div> </div> -<div id="inclusive-ancestor-inner-part"> +<div id="ancestor-inner-part"> <style> @container (width = 200px) { - #inclusive-ancestor-inner-part > div::part(inner-part) { color: green; } + #ancestor-inner-part > div::part(inner-part) { color: green; } } </style> <div> @@ -211,7 +211,7 @@ </div> </div> -<div id="inclusive-ancestor-slot-fallback"> +<div id="ancestor-slot-fallback"> <div><template shadowrootmode="open"> <style> div { @@ -266,9 +266,79 @@ <span id="t12" part="part"></span> </div> </template> + </div> +</div> + +<div id="named-part-host"> + <template shadowrootmode="open"> <style> + @container --part2 (width >= 0px) { + #named-part2-child { color: green; } + } + #named-part1 { + container: --part1 / inline-size; + color: green; + } </style> - </div> + <div id="named-part1" part="part1"> + <div id="named-part1-child" part="part1-child"></div> + </div> + <div part="part2"> + <div id="named-part2-child" part="part2-child"></div> + </div> + </template> + <style> + #named-part-host::part(part2) { container: --part2 / inline-size; } + @container --part1 (width >= 0px) { + #named-part-host::part(part1-child) { background-color: red; } + } + </style> +</div> + +<div id="named-slotted-host"> + <template shadowrootmode="open"> + <style> + @container --slot-container (width >= 0px) { + ::slotted(#slotted1) { color: green; } + } + #named-slotted-container { + container: --slot-container / inline-size; + } + </style> + <div id="named-slotted-container"> + <slot></slot> + </div> + </template> + <div id="slotted1"></div> + <div id="slotted2"></div> + <style> + @container --slot-container (width >= 0px) { + #slotted2 { color: red; } + } + </style> +</div> + +<div id="named-host"> + <template shadowrootmode="open"> + <style> + :host { + container: --host-container / inline-size; + } + @container --host-container (width >= 0px) { + #host-descendant { color: green; } + ::slotted(#host-slotted1) { color: green; } + } + </style> + <div id="host-descendant"></div> + <slot></slot> + </template> + <div id="host-slotted1"></div> + <div id="host-slotted2"></div> + <style> + @container --host-container (width >= 0px) { + #host-slotted2 { color: red; } + } + </style> </div> <script> @@ -277,9 +347,10 @@ }); const green = "rgb(0, 128, 0)"; + const red = "rgb(255, 0, 0)"; test(() => { - const t1 = document.querySelector("#inclusive-ancestor-across-root > div").shadowRoot.querySelector("#t1"); + const t1 = document.querySelector("#ancestor-across-root > div").shadowRoot.querySelector("#t1"); assert_equals(getComputedStyle(t1).color, green); }, "Match container in outer tree"); @@ -299,7 +370,7 @@ }, "Match container in outer tree for :host"); test(() => { - const t5 = document.querySelector("#inclusive-ancestor-part > div").shadowRoot.querySelector("#t5"); + const t5 = document.querySelector("#ancestor-part > div").shadowRoot.querySelector("#t5"); assert_equals(getComputedStyle(t5).color, green); }, "Match container in ::part selector's originating element tree"); @@ -314,19 +385,19 @@ }, "Match container in outer tree for :host::before"); test(() => { - const t8 = document.querySelector("#inclusive-ancestor-part-before > div").shadowRoot.querySelector("#t8"); + const t8 = document.querySelector("#ancestor-part-before > div").shadowRoot.querySelector("#t8"); assert_equals(getComputedStyle(t8, "::before").color, green); }, "Match container for ::before in ::part selector's originating element tree"); test(() => { - const outerhost = document.querySelector("#inclusive-ancestor-inner-part > div"); + const outerhost = document.querySelector("#ancestor-inner-part > div"); const innerhost = outerhost.shadowRoot.querySelector("div"); const t9 = innerhost.shadowRoot.querySelector("#t9"); assert_equals(getComputedStyle(t9).color, green); }, "Match container for ::part selector's originating element tree for exportparts"); test(() => { - const t10 = document.querySelector("#inclusive-ancestor-slot-fallback > div").shadowRoot.querySelector("#t10"); + const t10 = document.querySelector("#ancestor-slot-fallback > div").shadowRoot.querySelector("#t10"); assert_equals(getComputedStyle(t10).color, green); }, "Match container for slot light tree child fallback"); @@ -340,4 +411,39 @@ assert_equals(getComputedStyle(t12).color, green); }, "A :host::part rule should match containers in the originating element tree"); + test(() => { + const target = document.querySelector("#named-part-host").shadowRoot.querySelector("#named-part1-child"); + assert_equals(getComputedStyle(target).color, green); + }, "Container name set inside a shadow tree should not match query using ::part on the outside"); + + test(() => { + const target = document.querySelector("#named-part-host").shadowRoot.querySelector("#named-part2-child"); + assert_equals(getComputedStyle(target).color, green); + }, "Container name set with a ::part should match query inside the shadow tree"); + + test(() => { + const target = document.querySelector("#slotted1"); + assert_equals(getComputedStyle(target).color, green); + }, "Container name set inside a shadow tree should match query for a ::slotted() rule inside the tree"); + + test(() => { + const target = document.querySelector("#slotted2"); + assert_not_equals(getComputedStyle(target).color, red); + }, "Container name set inside a shadow tree should not match query for host child on the outside"); + + test(() => { + const target = document.querySelector("#named-host").shadowRoot.querySelector("#host-descendant"); + assert_equals(getComputedStyle(target).color, green); + }, "Container name set on :host from inside a shadow tree matching query inside the shadow tree"); + + test(() => { + const target = document.querySelector("#host-slotted1"); + assert_equals(getComputedStyle(target).color, green); + }, "Container name set on :host from inside a shadow tree matching query for ::slotted inside the shadow tree"); + + test(() => { + const target = document.querySelector("#host-slotted2"); + assert_not_equals(getComputedStyle(target).color, red); + }, "Container name set on :host from inside a shadow tree not matching query for slotted from the outside of the shadow tree"); + </script> diff --git a/tests/wpt/tests/css/css-contain/container-queries/style-container-for-shadow-dom.html b/tests/wpt/tests/css/css-contain/container-queries/style-container-for-shadow-dom.html index d88bf0bca98..e8297be941f 100644 --- a/tests/wpt/tests/css/css-contain/container-queries/style-container-for-shadow-dom.html +++ b/tests/wpt/tests/css/css-contain/container-queries/style-container-for-shadow-dom.html @@ -6,7 +6,7 @@ <script src="/resources/testharnessreport.js"></script> <script src="support/cq-testcommon.js"></script> -<div id="inclusive-ancestor-across-root"> +<div id="ancestor-across-root"> <div style="--foo: bar"> <template shadowrootmode="open"> <style> @@ -19,7 +19,7 @@ </div> </div> -<div id="inclusive-ancestor-skip-slotting"> +<div id="ancestor-slotting"> <div style="--foo: bar"> <template shadowrootmode="open"> <div style="--foo: baz"> @@ -27,7 +27,7 @@ </div> </template> <style> - @container style(--foo: bar) { + @container style(--foo: baz) { #t2 { color: green; } } </style> @@ -35,7 +35,7 @@ </div> </div> -<div id="inclusive-ancestor-slotted"> +<div id="ancestor-slotted"> <div style="--foo: baz"> <template shadowrootmode="open"> <style> @@ -49,7 +49,7 @@ </div> </div> -<div id="inclusive-ancestor-host" style="--foo: bar"> +<div id="ancestor-host" style="--foo: bar"> <div id="t4"> <template shadowrootmode="open"> <style> @@ -61,7 +61,7 @@ </div> </div> -<div id="inclusive-ancestor-part"> +<div id="ancestor-part"> <div style="--foo: bar"> <template shadowrootmode="open"> <div style="--foo: baz"> @@ -69,18 +69,18 @@ </div> </template> <style> - @container style(--foo: bar) { - #inclusive-ancestor-part > div::part(part) { color: green; } + @container style(--foo: baz) { + #ancestor-part > div::part(part) { color: green; } } </style> </div> </div> -<div id="inclusive-ancestor-slotted-before"> +<div id="ancestor-slotted-before"> <div> <template shadowrootmode="open"> <style> - @container style(--foo: bar) { + @container style(--foo: baz) { ::slotted(#t6)::before { content: "X"; color: green; @@ -93,7 +93,7 @@ </div> </div> -<div id="inclusive-ancestor-host-before"> +<div id="ancestor-host-before"> <div id="t7" style="--foo: bar"> <template shadowrootmode="open"> <style> @@ -108,10 +108,10 @@ </div> </div> -<div id="inclusive-ancestor-part-before"> +<div id="ancestor-part-before"> <style> - @container style(--foo: bar) { - #inclusive-ancestor-part-before > div::part(part)::before { + @container style(--foo: quz) { + #ancestor-part-before > div::part(part)::before { content: "X"; color: green; } @@ -120,23 +120,23 @@ <div style="--foo: bar"> <template shadowrootmode="open"> <div style="--foo: baz"> - <span id="t8" part="part"></span> + <span id="t8" part="part" style="--foo: quz"></span> </div> </template> </div> </div> -<div id="inclusive-ancestor-inner-part"> +<div id="ancestor-inner-part"> <style> - @container style(--foo: bar) { - #inclusive-ancestor-inner-part > div::part(inner-part) { color: green; } + @container style(--foo: quz) { + #ancestor-inner-part > div::part(inner-part) { color: green; } } </style> <div style="--foo: bar"> <template shadowrootmode="open"> - <div exportparts="inner-part" style="-foo: baz"> + <div exportparts="inner-part" style="--foo: baz"> <template shadowrootmode="open"> - <div style="--foo: baz"> + <div style="--foo: quz"> <span id="t9" part="inner-part"></span> </div> </template> @@ -145,7 +145,7 @@ </div> </div> -<div id="inclusive-ancestor-slot-fallback"> +<div id="ancestor-slot-fallback"> <div><template shadowrootmode="open"> <style> @container style(--foo: bar) { @@ -158,19 +158,16 @@ </template></div> </div> -<div id="no-container-for-part"> +<div id="container-for-part"> <div> <template shadowrootmode="open"> - <style> - #t11 { color: green; } - </style> <div style="--foo: bar"> <span id="t11" part="part"></span> </div> </template> <style> @container style(--foo: bar) { - #no-container-for-part > div::part(part) { color: red; } + #container-for-part > div::part(part) { color: green; } } </style> </div> @@ -180,7 +177,7 @@ <div> <template shadowrootmode="open"> <style> - @container style(--foo: bar) { + @container style(--foo: baz) { :host::part(part) { color: green; } } </style> @@ -199,19 +196,19 @@ const green = "rgb(0, 128, 0)"; test(() => { - const t1 = document.querySelector("#inclusive-ancestor-across-root > div").shadowRoot.querySelector("#t1"); + const t1 = document.querySelector("#ancestor-across-root > div").shadowRoot.querySelector("#t1"); assert_equals(getComputedStyle(t1).color, green); }, "Match container in outer tree"); test(() => { const t2 = document.querySelector("#t2"); assert_equals(getComputedStyle(t2).color, green); - }, "Match container in same tree, not walking flat tree ancestors"); + }, "Match container in the shadow tree for a host child in the host child's tree scope"); test(() => { const t3 = document.querySelector("#t3"); assert_equals(getComputedStyle(t3).color, green); - }, "Match container in ::slotted selector's originating element tree"); + }, "Match <slot> as a container for ::slotted element"); test(() => { const t4 = document.querySelector("#t4"); @@ -219,14 +216,14 @@ }, "Match container in outer tree for :host"); test(() => { - const t5 = document.querySelector("#inclusive-ancestor-part > div").shadowRoot.querySelector("#t5"); + const t5 = document.querySelector("#ancestor-part > div").shadowRoot.querySelector("#t5"); assert_equals(getComputedStyle(t5).color, green); - }, "Match container in ::part selector's originating element tree"); + }, "Match ::part's parent in the shadow tree as the container for ::part"); test(() => { const t6 = document.querySelector("#t6"); assert_equals(getComputedStyle(t6, "::before").color, green); - }, "Match container for ::before in ::slotted selector's originating element tree"); + }, "Match ::slotted as a container for its ::before"); test(() => { const t7 = document.querySelector("#t7"); @@ -234,30 +231,30 @@ }, "Match container in outer tree for :host::before"); test(() => { - const t8 = document.querySelector("#inclusive-ancestor-part-before > div").shadowRoot.querySelector("#t8"); + const t8 = document.querySelector("#ancestor-part-before > div").shadowRoot.querySelector("#t8"); assert_equals(getComputedStyle(t8, "::before").color, green); - }, "Match container for ::before in ::part selector's originating element tree"); + }, "Match the ::part as a container for ::before on ::part elements"); test(() => { - const outerhost = document.querySelector("#inclusive-ancestor-inner-part > div"); + const outerhost = document.querySelector("#ancestor-inner-part > div"); const innerhost = outerhost.shadowRoot.querySelector("div"); const t9 = innerhost.shadowRoot.querySelector("#t9"); assert_equals(getComputedStyle(t9).color, green); - }, "Match container for ::part selector's originating element tree for exportparts"); + }, "Match container for ::part selector in inner shadow tree for exportparts"); test(() => { - const t10 = document.querySelector("#inclusive-ancestor-slot-fallback > div").shadowRoot.querySelector("#t10"); + const t10 = document.querySelector("#ancestor-slot-fallback > div").shadowRoot.querySelector("#t10"); assert_equals(getComputedStyle(t10).color, green); }, "Match container for slot light tree child fallback"); test(() => { - const t11 = document.querySelector("#no-container-for-part > div").shadowRoot.querySelector("#t11"); + const t11 = document.querySelector("#container-for-part > div").shadowRoot.querySelector("#t11"); assert_equals(getComputedStyle(t11).color, green); - }, "Should not match container inside shadow tree for ::part()"); + }, "Should match parent container inside shadow tree for ::part()"); test(() => { const t12 = document.querySelector("#inner-scope-host-part > div").shadowRoot.querySelector("#t12"); assert_equals(getComputedStyle(t12).color, green); - }, "A :host::part rule should match containers in the originating element tree"); + }, "A :host::part rule matching a container in the shadow tree"); </script> diff --git a/tests/wpt/tests/css/css-flexbox/remove-wrapped-001.html b/tests/wpt/tests/css/css-flexbox/remove-wrapped-001.html new file mode 100644 index 00000000000..fd38ccce27a --- /dev/null +++ b/tests/wpt/tests/css/css-flexbox/remove-wrapped-001.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1902156"> +<link rel="help" href="https://drafts.csswg.org/css-flexbox/#flex-items"> +<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht"> +<link rel="author" href="mailto:dholbert@mozilla.com" title="Daniel Holbert"> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel="author" href="https://mozilla.org" title="Mozilla"> +<style> +#flex { + display: flex; + width: 100px; + justify-content: center; + background: red; + /* This gap ensures that we'll see some of the red background, + if there ends up being more than 1 flex item. */ + gap: 90px; +} +#swatch { + width: 100px; + height: 100px; + background: green; +} +</style> +<script> + function go() { + flex.getBoundingClientRect(); + flex.removeChild(flex.lastChild); + } +</script> +<body onload="go()"> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<!-- The fact that we have (and take) a linewrap opportunity + in "foo bar" is important be able to trigger the bug: --> +<div id="flex"><div id="swatch"></div>foo bar</div> +</body> diff --git a/tests/wpt/tests/css/css-flexbox/remove-wrapped-002.html b/tests/wpt/tests/css/css-flexbox/remove-wrapped-002.html new file mode 100644 index 00000000000..a8048b84a3b --- /dev/null +++ b/tests/wpt/tests/css/css-flexbox/remove-wrapped-002.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1902156"> +<link rel="help" href="https://drafts.csswg.org/css-flexbox/#flex-items"> +<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht"> +<link rel="author" href="mailto:dholbert@mozilla.com" title="Daniel Holbert"> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel="author" href="https://mozilla.org" title="Mozilla"> +<style> +#flex { + display: flex; + width: 100px; + justify-content: center; + background: red; + /* This gap ensures that we'll see some of the red background, + if there ends up being more than 1 flex item. */ + gap: 90px; +} +#swatch { + width: 100px; + height: 100px; + background: green; +} +</style> +<script> + function go() { + /* The fact that we have (and take) a linewrap opportunity + in "foo bar" is important be able to trigger the bug: */ + let whitespace = document.createTextNode(" "); + let text = document.createTextNode("foo bar"); + flex.appendChild(whitespace); + flex.appendChild(text); + flex.offsetHeight; + text.remove(); + } +</script> +<body onload="go()"> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="flex"><div id="swatch"></div></div> diff --git a/tests/wpt/tests/css/css-fonts/font-variant-emoji-003-ref.html b/tests/wpt/tests/css/css-fonts/font-variant-emoji-003-ref.html new file mode 100644 index 00000000000..1e8cdb89cfa --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/font-variant-emoji-003-ref.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Fonts: font-variant-emoji web font test</title> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<style> + .color { + font-family: "ColorEmojiFont"; + } + + .mono { + font-family: "MonoEmojiFont"; + } +</style> + +<p>Emoji Default</p> +<div> + <span class="color">🫨</span> + <span class="mono">🫨</span> + <span class="color">🫨</span> +</div> + +<p>Text Default</p> +<div> + <span class="color">ℹ</span> + <span class="mono">ℹ</span> + <span class="mono">ℹ</span> +</div> diff --git a/tests/wpt/tests/css/css-fonts/font-variant-emoji-003.html b/tests/wpt/tests/css/css-fonts/font-variant-emoji-003.html new file mode 100644 index 00000000000..e197afa7c40 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/font-variant-emoji-003.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Fonts: font-variant-emoji web font test</title> +<link rel="help" href="https://www.w3.org/TR/css-fonts-4/#font-variant-emoji-prop" /> +<link rel="help" href="https://www.unicode.org/reports/tr51" /> +<link rel="match" href="font-variant-emoji-003-ref.html"/> +<meta name="assert" content="font-variant-emoji sets an emoji's presentation for the emoji default and text default emoji codepoints and affects the fallback font selection."/> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<style> + .emoji { + font-variant-emoji: emoji; + } + + .text { + font-variant-emoji: text; + } + + .unicode { + font-variant-emoji: unicode; + } +</style> + +<p>Emoji Default</p> +<!-- Codepoint U+1FAE8 has emoji presentation by default, +(Emoji=Yes,Emoji_Presentation=Yes). +See https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt. +So when font-variant-emoji is set to `unicode`, the codepoint +should be displayed in color. --> +<div style="font-family: MonoEmojiFont, ColorEmojiFont;"> + <span class="emoji">🫨</span> + <span class="text">🫨</span> + <span class="unicode">🫨</span> +</div> + +<p>Text Default</p> +<!-- Codepoint U+2139 has text presentation by default, +(Emoji=Yes,Emoji_Presentation=No). +See https://www.unicode.org/Public/15.1.0/ucd/emoji/emoji-data.txt. +So when font-variant-emoji is set to `unicode`, the codepoint +should be displayed in monochrome. --> +<div style="font-family: ColorEmojiFont, MonoEmojiFont;"> + <span class="emoji">ℹ</span> + <span class="text">ℹ</span> + <span class="unicode">ℹ</span> +</div> diff --git a/tests/wpt/tests/css/css-fonts/font-variant-emoji-004-ref.html b/tests/wpt/tests/css/css-fonts/font-variant-emoji-004-ref.html new file mode 100644 index 00000000000..b15c8227cc4 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/font-variant-emoji-004-ref.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Fonts: font-variant-emoji web font test</title> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<style> + .color { + font-family: "ColorEmojiFont"; + } + + .mono { + font-family: "MonoEmojiFont"; + } +</style> + +<p>Variation Selector 15</p> +<div class="mono"> + <span>🫨</span> + <span>🫨</span> + <span>🫨</span> + <span>ℹ</span> +</div> + +<p>Variation Selector 16</p> +<div class="color"> + <span>🫨</span> + <span>🫨</span> + <span>🫨</span> + <span>ℹ</span> +</div>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-fonts/font-variant-emoji-004.html b/tests/wpt/tests/css/css-fonts/font-variant-emoji-004.html new file mode 100644 index 00000000000..d76828dff01 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/font-variant-emoji-004.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Fonts: font-variant-emoji web font test</title> +<link rel="help" href="https://www.w3.org/TR/css-fonts-4/#font-variant-emoji-prop" /> +<link rel="help" href="https://www.unicode.org/reports/tr51/tr51-25.html#Emoji_Properties_and_Data_Files" /> +<link rel="match" href="font-variant-emoji-004-ref.html"/> +<meta name="assert" content="Variation Selector 15 (U+FE0E) or Variation Selector 16 (U+FE0F) in the contents of an element override the rendering specified by font-variant-emoji property."/> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<style> + .emoji { + font-variant-emoji: emoji; + } + + .text { + font-variant-emoji: text; + } + + .unicode { + font-variant-emoji: unicode; + } +</style> + +<p>Variation Selector 15</p> +<div style="font-family: ColorEmojiFont, MonoEmojiFont;"> + <span class="text">🫨︎</span> + <span class="emoji">🫨︎</span> + <span class="unicode">🫨︎</span> + <span class="unicode">ℹ︎</span> +</div> + +<p>Variation Selector 16</p> +<div style="font-family: MonoEmojiFont, ColorEmojiFont;"> + <span class="text">🫨️</span> + <span class="emoji">🫨️</span> + <span class="unicode">🫨️</span> + <span class="unicode">ℹ️</span> +</div> 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 differindex 24ab79fd052..8754e127d7f 100644 --- 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 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 differindex 0b054c7c8d5..f9996293bf6 100644 --- 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 diff --git a/tests/wpt/tests/css/css-grid/empty-grid-within-flexbox.html b/tests/wpt/tests/css/css-grid/empty-grid-within-flexbox.html index 1941800cb18..ee67486474a 100644 --- a/tests/wpt/tests/css/css-grid/empty-grid-within-flexbox.html +++ b/tests/wpt/tests/css/css-grid/empty-grid-within-flexbox.html @@ -4,7 +4,7 @@ <link rel="author" title="Ethan Jimenez" href="mailto:ethavar@microsoft.com"/> <link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1246609"> <link rel="match" href="../reference/ref-filled-green-100px-square.xht"> -<meta name="assert" content="Checks that the intrisic sizes of an empty grid are correctly computed."> +<meta name="assert" content="Checks that the intrinsic sizes of an empty grid are correctly computed."> <style> .flex { display: flex; diff --git a/tests/wpt/tests/css/css-inline/initial-letter/crashtests/initial-letter-dynamic-crash.html b/tests/wpt/tests/css/css-inline/initial-letter/crashtests/initial-letter-dynamic-crash.html new file mode 100644 index 00000000000..f20f6733114 --- /dev/null +++ b/tests/wpt/tests/css/css-inline/initial-letter/crashtests/initial-letter-dynamic-crash.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<link rel="help" href="crbug.com/342802976"> +<style> +p::first-letter { + background-image: url(../../../../images/blue.png); +} +p.initial-letter::first-letter { + initial-letter: 2; +} +</style> +<p id="target" class="initial-letter">Test</p> +<script> +document.body.offsetTop; +target.className = ''; +</script> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001-ref.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001-ref.html new file mode 100644 index 00000000000..9bd12cb0bdf --- /dev/null +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> +<style> +.spacer { + background: lightgray; + block-size: 100px; +} +.target { + font-family: Ahem; + font-size: 100px; + line-height: 1; + height: 120px; +} +</style> +<div class="spacer"></div> +<div class="target">A</div> +<div class="spacer"></div> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001.html new file mode 100644 index 00000000000..d732592e51a --- /dev/null +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-001.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Tests the `height` is the specified value when `text-box-trim` is applied</title> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#propdef-text-box-edge"> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#propdef-text-box-trim"> +<link rel="match" href="text-box-trim-height-001-ref.html"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> +<style> +.spacer { + background: lightgray; + block-size: 100px; +} +.target { + font-family: Ahem; + font-size: 100px; + line-height: 2; + height: 120px; + text-box-trim: both; + text-box-edge: text alphabetic; +} +</style> +<div class="spacer"></div> +<div class="target">A</div> +<div class="spacer"></div> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-002.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-002.html new file mode 100644 index 00000000000..1249627f838 --- /dev/null +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-height-002.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Tests the `height` is the specified value when `text-box-trim` is applied</title> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#propdef-text-box-edge"> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#propdef-text-box-trim"> +<link rel="match" href="text-box-trim-height-001-ref.html"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> +<style> +.spacer { + background: lightgray; + block-size: 100px; +} +.target { + font-family: Ahem; + font-size: 100px; + line-height: 2; + min-height: 120px; + text-box-trim: both; + text-box-edge: text alphabetic; +} +</style> +<div class="spacer"></div> +<div class="target">A</div> +<div class="spacer"></div> diff --git a/tests/wpt/tests/css/css-lists/counter-set-001-ref.html b/tests/wpt/tests/css/css-lists/counter-set-001-ref.html index 2b5eb90ccfc..301197904a5 100644 --- a/tests/wpt/tests/css/css-lists/counter-set-001-ref.html +++ b/tests/wpt/tests/css/css-lists/counter-set-001-ref.html @@ -21,17 +21,17 @@ html,body { <span>6</span><!-- "6" --> <span>0</span><!-- "0" --> <x> - <span>0.2</span><!-- "0.2" --> + <span>2</span><!-- "2" --> </x> -<span>0.3</span><!-- "0.3" --> +<span>3</span><!-- "3" --> <x> - <span>0.3.0</span><!-- "0.3.0" --> - <span>0.3.2</span><!-- "0.3.2" --> + <span>0</span><!-- "0" --> + <span>2</span><!-- "2" --> <x> - <span>0.3.2.5</span><!-- "0.3.2.5" --> + <span>2.5</span><!-- "2.5" --> </x> </x> -<span>0.3.3</span><!-- "0.3.3" --> +<span>3</span><!-- "3" --> </body> </html> diff --git a/tests/wpt/tests/css/css-lists/counter-set-001.html b/tests/wpt/tests/css/css-lists/counter-set-001.html index 6ec53e3bfa1..4e28367798a 100644 --- a/tests/wpt/tests/css/css-lists/counter-set-001.html +++ b/tests/wpt/tests/css/css-lists/counter-set-001.html @@ -24,17 +24,17 @@ span::before { content: counters(n, '.'); } <span style="counter-set: n 6; counter-increment: n 2"></span><!-- "6" --> <span style="counter-set: n; counter-increment: n 2"></span><!-- "0" --> <x style="counter-reset: n 9"> - <span style="counter-set: n 2"></span><!-- "0.2" --> + <span style="counter-set: n 2"></span><!-- "2" --> </x> -<span style="counter-increment: n"></span><!-- "0.3" --> +<span style="counter-increment: n"></span><!-- "3" --> <x style="counter-reset: n 9"> - <span style="counter-set: n"></span><!-- "0.3.0" --> - <span style="counter-set: n 2"></span><!-- "0.3.2" --> + <span style="counter-set: n"></span><!-- "0" --> + <span style="counter-set: n 2"></span><!-- "2" --> <x style="counter-reset: n 1"> - <span style="counter-set: n 5"></span><!-- "0.3.2.5" --> + <span style="counter-set: n 5"></span><!-- "2.5" --> </x> </x> -<span style="counter-increment: n"></span><!-- "0.3.3" --> +<span style="counter-increment: n"></span><!-- "3" --> </body> </html> diff --git a/tests/wpt/tests/css/css-lists/counters-005-ref.html b/tests/wpt/tests/css/css-lists/counters-005-ref.html index 232bab76a10..509a097ef46 100644 --- a/tests/wpt/tests/css/css-lists/counters-005-ref.html +++ b/tests/wpt/tests/css/css-lists/counters-005-ref.html @@ -12,8 +12,8 @@ <p>The following two lines should be identical:</p> - <div>B 1 1.0 1 1.0</div> - <div>B 1 1.0 1 1.0</div> + <div>B 1 0 1 1.0</div> + <div>B 1 0 1 1.0</div> </body> </html> diff --git a/tests/wpt/tests/css/css-lists/counters-005.html b/tests/wpt/tests/css/css-lists/counters-005.html index 12908e64720..528c0ede870 100644 --- a/tests/wpt/tests/css/css-lists/counters-005.html +++ b/tests/wpt/tests/css/css-lists/counters-005.html @@ -32,7 +32,7 @@ </span> </div> - <div>B 1 1.0 1 1.0</div> + <div>B 1 0 1 1.0</div> </body> </html> diff --git a/tests/wpt/tests/css/css-lists/counters-006-ref.html b/tests/wpt/tests/css/css-lists/counters-006-ref.html new file mode 100644 index 00000000000..8e21dca4cee --- /dev/null +++ b/tests/wpt/tests/css/css-lists/counters-006-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>CSS Lists Ref Test: counters instantiating</title> +<link rel="author" title="Daniil Sakhapov" href="mailto:sakhapov@chromium.org"> +<link rel="help" href="https://www.w3.org/TR/css-lists-3/#instantiating-counters"> +<style type="text/css"> + * { + padding: 0; + margin: 0; + } +</style> + +<div>1.1.1.1</div> +<div>1.2.1.2</div> +<div>1.3.1.3</div> +<div>2.1.2.1</div> +<div>2.2.2.2</div> +<div>2.3.2.3</div> diff --git a/tests/wpt/tests/css/css-lists/counters-006.html b/tests/wpt/tests/css/css-lists/counters-006.html new file mode 100644 index 00000000000..63a65a7ff83 --- /dev/null +++ b/tests/wpt/tests/css/css-lists/counters-006.html @@ -0,0 +1,38 @@ +<!doctype html> +<title>CSS Lists: counters instantiating</title> +<link rel="author" title="Daniil Sakhapov" href="mailto:sakhapov@chromium.org"> +<link rel="help" href="https://www.w3.org/TR/css-lists-3/#instantiating-counters"> +<link rel="match" href="counters-006-ref.html"> +<style type="text/css"> + * { + padding: 0; + margin: 0; + } + + .test { + counter-increment: section; + counter-reset: multilevel; + } + + ol[multilevel] > li::before { + content: counter(section) "." counters(multilevel, ".") "."; + counter-increment: multilevel; + } + + ol[multilevel] { + list-style: none !important; + clear: both; + } +</style> +<div class="test"></div> +<ol multilevel="multilevel"> + <li>1.1</li> + <li>1.2</li> + <li>1.3</li> +</ol> +<div class="test"></div> +<ol multilevel="multilevel"> + <li>2.1</li> + <li>2.2</li> + <li>2.3</li> +</ol> diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-001.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-001.html index c8cfcb1066d..c8cfcb1066d 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-001.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-001.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-002.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-002.html index 5f21b545fb3..5f21b545fb3 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-002.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-002.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-003.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-003.html index fa3b7472e5e..fa3b7472e5e 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-003.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-003.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-004.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-004.html index c766d195b7c..c766d195b7c 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-004.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-004.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-005.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-005.tentative.html index 143aa65d899..143aa65d899 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-005.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-005.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-006.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-006.tentative.html index f06d94161b6..f06d94161b6 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-006.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-006.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-007.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-007.tentative.html index c71068641b8..c71068641b8 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-007.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-007.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-008.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-008.tentative.html index 0d91b3612d0..0d91b3612d0 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-008.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-008.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-009.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-009.tentative.html index 4dfd3d6194a..4dfd3d6194a 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-009.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-009.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-010.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-010.html index 1386b147ce6..1386b147ce6 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-010.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-010.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-011.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-011.tentative.html index 953f0c4faa9..953f0c4faa9 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-011.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-011.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-012.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-012.tentative.html index be390740377..be390740377 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-012.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-012.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-013.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-013.tentative.html index 1bda501f028..1bda501f028 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-013.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-013.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-014.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-014.html index 9ca7c893724..9ca7c893724 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-014.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-014.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-015.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-015.html index 82030073223..82030073223 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-015.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-015.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-016.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-016.tentative.html index 09714c499de..09714c499de 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-016.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-016.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-017.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-017.tentative.html index 11d6ceeb556..11d6ceeb556 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-017.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-017.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-018.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-018.tentative.html index af75f7dfb0d..af75f7dfb0d 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-018.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-018.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-019.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-019.tentative.html index b39376d3950..b39376d3950 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-019.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-019.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-020.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-020.html index 9d8a2b4d06c..9d8a2b4d06c 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-020.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-020.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-021.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-021.tentative.html index 611cd6f8901..611cd6f8901 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-021.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-021.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-001.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html index 02d8479736d..02d8479736d 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-001.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-001.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-002.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html index ff9e802f0f9..ff9e802f0f9 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-002.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-002.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-003.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html index a74704dd3b0..a74704dd3b0 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-003.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-003.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-004.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html index 2dbf9d54084..2dbf9d54084 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-004.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-004.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-005.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html index 1c9148e2647..1c9148e2647 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-005.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-005.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-006.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-006.tentative.html index ab6915f5216..ab6915f5216 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-006.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-006.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-007.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-007.tentative.html index f7d56bfa655..f7d56bfa655 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-007.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-007.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-008.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-008.tentative.html index 9e7f38ab7ca..9e7f38ab7ca 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-008.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-008.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-009.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html index 44d111056c6..44d111056c6 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-009.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-009.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-010.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-010.tentative.html index cb706bba08e..cb706bba08e 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-010.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-010.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-011.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html index 00076a5336e..00076a5336e 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-011.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-011.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-012.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-012.tentative.html index 56957b15c92..56957b15c92 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-012.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-012.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-013.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html index dd864fbdcb3..dd864fbdcb3 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-013.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-013.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-014.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html index 6738c708706..6738c708706 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-014.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-014.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-015.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html index cdb1ed18c0d..cdb1ed18c0d 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-015.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-015.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-auto-016.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html index 372213983b7..372213983b7 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-auto-016.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-auto-016.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-001.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html index 79667f23fbd..79667f23fbd 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-001.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-001.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-002.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html index cecb9d52bc6..cecb9d52bc6 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-002.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-002.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-003.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html index e4bd1de222e..e4bd1de222e 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-003.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-003.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-004.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html index 483e6d1da6d..483e6d1da6d 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-004.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-004.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-005.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html index 3dc77831a06..3dc77831a06 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-005.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-005.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-006.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html index f18fed6c2da..f18fed6c2da 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-006.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-006.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-007.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html index f0a1f58c8d3..f0a1f58c8d3 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-007.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-007.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-008.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html index 9c62e44f389..9c62e44f389 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-008.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-008.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-009.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html index dce04d720cf..dce04d720cf 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-009.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-009.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-010.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html index 325278b3a0b..325278b3a0b 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-010.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-010.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html index ab5102a7cf2..ab5102a7cf2 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-abspos-011.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-abspos-011.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html index 98bbdcb9040..98bbdcb9040 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-001.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-001.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html index 15379a3de3e..15379a3de3e 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-002.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-002.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html index c2037582351..c2037582351 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-003.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-003.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html index 62131301747..62131301747 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-004.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-004.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html index 9689a83fa7f..9689a83fa7f 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-005.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-005.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html index 0a709bff198..0a709bff198 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-006.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-006.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html index 7ee286fbf43..7ee286fbf43 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-007.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-007.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html index c62ba5371a8..c62ba5371a8 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-008.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-008.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html index f25ac381c09..f25ac381c09 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-009.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-009.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-010.tentative.html index a00ff601712..a00ff601712 100644 --- a/tests/wpt/tests/css/css-overflow/line-clamp-with-floats-010.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/line-clamp-with-floats-010.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-010-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-010-ref.html index 46ca731c544..46ca731c544 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-010-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-010-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-011-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-011-ref.html index 04297fff2b5..04297fff2b5 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-011-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-011-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-012-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-012-ref.html index f412e0110d4..f412e0110d4 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-012-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-012-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-013-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-013-ref.html index b2eb05e884d..b2eb05e884d 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-013-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-013-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-015-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-015-ref.html index 1af45c1225c..1af45c1225c 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-015-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-015-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-021-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-021-ref.html index d794c76e3c9..d794c76e3c9 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-021-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-021-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-002-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-002-ref.html index fe0a8dbd588..fe0a8dbd588 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-002-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-002-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-005-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-005-ref.html index 01eea67a0da..01eea67a0da 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-005-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-005-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-011-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-011-ref.html index 5f7120ee39a..5f7120ee39a 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-011-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-011-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-016-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-016-ref.html index 4a5f3536cc6..4a5f3536cc6 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-auto-016-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-auto-016-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-001-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-001-ref.html index d756162dde0..d756162dde0 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-001-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-001-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-005-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-005-ref.html index 3b1f9218e88..3b1f9218e88 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-005-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-005-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-006-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-006-ref.html index 4b55c37a033..4b55c37a033 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-006-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-006-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-007-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-007-ref.html index e3dcc696e39..e3dcc696e39 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-007-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-007-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-008-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-008-ref.html index 373b2755c1d..373b2755c1d 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-008-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-008-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-010-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-010-ref.html index ecc2fcee1b5..ecc2fcee1b5 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-010-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-010-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-011-ref.html index f08b0270e8d..f08b0270e8d 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-abspos-011-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-abspos-011-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-001-ref.html index bcd70aa9305..bcd70aa9305 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-001-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-001-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-005-ref.html index d20d6c53dde..d20d6c53dde 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-005-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-005-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-006-ref.html index 9288c4e36f9..9288c4e36f9 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-006-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-006-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-007-ref.html index 6d5390246b4..6d5390246b4 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-007-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-007-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-008-ref.html index 50b3d53900e..50b3d53900e 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-008-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-008-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-010-ref.html index 12b8cdc441a..12b8cdc441a 100644 --- a/tests/wpt/tests/css/css-overflow/reference/line-clamp-with-floats-010-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/line-clamp-with-floats-010-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-001-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html index ef28e01dac0..ef28e01dac0 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-001-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-001-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-005-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html index c9a9ae5d7ff..c9a9ae5d7ff 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-005-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-005-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-006-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-006-ref.html index 3a17a6d5f3b..3a17a6d5f3b 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-006-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-006-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-007-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-007-ref.html index 1c9a728aafc..1c9a728aafc 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-007-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-007-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-008-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-008-ref.html index 4ead437830f..4ead437830f 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-008-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-008-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-009-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-009-ref.html index 62e100d6334..62e100d6334 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-009-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-009-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-010-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-010-ref.html index 63e9885e8b9..63e9885e8b9 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-010-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-010-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-011-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-011-ref.html index fbcd8ca3115..fbcd8ca3115 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-011-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-011-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-012-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-012-ref.html index d28c9388aea..d28c9388aea 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-012-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-012-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-013-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-013-ref.html index ba14aefc7cc..ba14aefc7cc 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-013-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-013-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-014-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-014-ref.html index c7346ab5279..c7346ab5279 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-014-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-014-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-015-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-015-ref.html index d73bc7d7cb6..d73bc7d7cb6 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-015-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-015-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-016-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-016-ref.html index e54947e3956..e54947e3956 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-016-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-016-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-017-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-017-ref.html index 043e5f3d1c3..043e5f3d1c3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-017-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-017-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-018-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-018-ref.html index b95f7ce7662..b95f7ce7662 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-018-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-018-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-019-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-019-ref.html index b95f7ce7662..b95f7ce7662 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-019-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-019-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-020-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-020-ref.html index 043e5f3d1c3..043e5f3d1c3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-020-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-020-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-021-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-021-ref.html index 043e5f3d1c3..043e5f3d1c3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-021-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-021-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-022-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-022-ref.html index 29d738589d1..29d738589d1 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-022-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-022-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-023-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-023-ref.html index 3127bb078b3..3127bb078b3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-023-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-023-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-024-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-024-ref.html index d4820a1b487..d4820a1b487 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-024-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-024-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-025-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-025-ref.html index c33a6b50338..c33a6b50338 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-025-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-025-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-026-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-026-ref.html index e4c1b033499..e4c1b033499 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-026-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-026-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-027-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-027-ref.html index be27d6d276a..be27d6d276a 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-027-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-027-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-029-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-029-ref.html index 0d18c2a7f55..0d18c2a7f55 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-029-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-029-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-030-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-030-ref.html index e88c8bffc02..e88c8bffc02 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-030-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-030-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-031-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-031-ref.html index 14f2fb1bb40..14f2fb1bb40 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-031-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-031-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-032-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-032-ref.html index 8f5b1ef53eb..8f5b1ef53eb 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-032-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-032-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-035-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-035-ref.html index 230218399e3..230218399e3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-035-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-035-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-036-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-036-ref.html index 0de35e98ab3..0de35e98ab3 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-036-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-036-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-037-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-037-ref.html index 2927716d76d..2927716d76d 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-037-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-037-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-038-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-038-ref.html index bd8db762dd8..bd8db762dd8 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-038-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-038-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-039-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-039-ref.html index fd8a76b2f8e..fd8a76b2f8e 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-039-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-039-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-040-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-040-ref.html index f55be86e546..f55be86e546 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-040-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-040-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-block-in-inline-001-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-block-in-inline-001-ref.html index 79f2e409109..79f2e409109 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-block-in-inline-001-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-block-in-inline-001-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-dynamic-001-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-dynamic-001-ref.html index 21458953df9..21458953df9 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-dynamic-001-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-dynamic-001-ref.html diff --git a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-with-line-height-ref.html b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-with-line-height-ref.html index c2c8914161a..c2c8914161a 100644 --- a/tests/wpt/tests/css/css-overflow/reference/webkit-line-clamp-with-line-height-ref.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/reference/webkit-line-clamp-with-line-height-ref.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-001.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-001.html index bba3d1c49f2..bba3d1c49f2 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-001.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-001.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-002.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-002.html index a04b9599420..a04b9599420 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-002.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-002.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-003.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-003.html index 5ebe64675f8..5ebe64675f8 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-003.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-003.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-004.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-004.html index d66d0bb62f1..d66d0bb62f1 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-004.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-004.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-005.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-005.html index 73b4b8cfa8b..73b4b8cfa8b 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-005.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-005.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-006.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-006.html index 0d2c1f9c645..0d2c1f9c645 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-006.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-006.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-007.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-007.html index 95c2db51653..95c2db51653 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-007.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-007.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-008.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-008.html index 471333e666f..471333e666f 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-008.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-008.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-009.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-009.html index 240f6b113b0..240f6b113b0 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-009.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-009.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-010.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-010.html index 9ff23a2fe39..9ff23a2fe39 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-010.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-010.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-011.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-011.html index 3a0016e9dfd..3a0016e9dfd 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-011.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-011.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-012.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-012.html index d46a7944faf..d46a7944faf 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-012.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-012.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-013.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-013.html index 6db53906acd..6db53906acd 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-013.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-013.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-014.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-014.html index 948b2475330..948b2475330 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-014.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-014.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-015.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-015.html index c1817e04e6a..c1817e04e6a 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-015.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-015.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-016.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-016.html index 5b2d4593aef..5b2d4593aef 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-016.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-016.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-017.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-017.html index ece4d3123b1..ece4d3123b1 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-017.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-017.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-018.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-018.html index d381c645467..d381c645467 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-018.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-018.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-019.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-019.html index ff6f7e3cf78..ff6f7e3cf78 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-019.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-019.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-020.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-020.html index 9d875d52dee..9d875d52dee 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-020.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-020.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-021.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-021.html index 44a196c9ba1..44a196c9ba1 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-021.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-021.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-022.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-022.html index 2959a734411..2959a734411 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-022.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-022.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-023.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-023.html index 7f03513baa8..7f03513baa8 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-023.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-023.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-024.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-024.html index 6ebf5a8958b..6ebf5a8958b 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-024.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-024.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-025.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-025.html index 67804dedf60..67804dedf60 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-025.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-025.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-026.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-026.html index 741384f2927..741384f2927 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-026.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-026.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-027.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-027.html index 93bfb0f4b46..93bfb0f4b46 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-027.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-027.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-029.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-029.html index fc395d313ea..fc395d313ea 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-029.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-029.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-030.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-030.html index 5e14a78a583..5e14a78a583 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-030.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-030.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-031.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-031.html index 4f8c55301ab..4f8c55301ab 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-031.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-031.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-032.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-032.html index 4d0731379d0..4d0731379d0 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-032.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-032.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-033.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-033.html index 261cf5b6b24..261cf5b6b24 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-033.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-033.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-034.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-034.html index e0b31d544c2..e0b31d544c2 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-034.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-034.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-035.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-035.html index 5a550e022ec..5a550e022ec 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-035.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-035.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-036.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-036.html index b8d7b194f05..b8d7b194f05 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-036.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-036.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-037.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-037.html index 86015228f88..86015228f88 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-037.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-037.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-038.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-038.html index b50ae2764c3..b50ae2764c3 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-038.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-038.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-039.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-039.html index fe9436da53d..fe9436da53d 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-039.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-039.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-040.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-040.html index fa885c8088d..fa885c8088d 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-040.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-040.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-041-crash.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-041-crash.html index 3f4bf3c8ef5..3f4bf3c8ef5 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-041-crash.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-041-crash.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-042-crash.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-042-crash.html index 1a5fe2e246f..1a5fe2e246f 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-042-crash.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-042-crash.html diff --git a/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-043.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-043.html new file mode 100644 index 00000000000..1f7a0a94c03 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-043.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org"> +<link rel="help" href="https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div style="columns:3; column-fill:auto; gap:0; width:300px; height:250px;"> + <div style="display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:clip; line-height:50px; color:transparent; background:green;"> + <br> + <br> + <span style="color:red;"> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + FAIL<br> + </span> + </div> +</div> diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-block-in-inline-001.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-block-in-inline-001.html index 75d1de3bf5b..75d1de3bf5b 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-block-in-inline-001.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-block-in-inline-001.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-dynamic-001.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-dynamic-001.html index fc4f2f9e4db..fc4f2f9e4db 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-dynamic-001.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-dynamic-001.html diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html index a82635f986b..a82635f986b 100644 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-with-line-height.tentative.html +++ b/tests/wpt/tests/css/css-overflow/line-clamp/webkit-line-clamp-with-line-height.tentative.html diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-001-ref.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-001-ref.html new file mode 100644 index 00000000000..5a89c88467e --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-001-ref.html @@ -0,0 +1,412 @@ + +<!DOCTYPE html> +<html> +<title>Scrollable Area with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box */ + .indicator { + width: 72px; + height: 72px; + background: green; + } + + /* Create a test box with appropriate scrollbars */ + .test { + width: 24px; + height: 24px; + overflow: hidden; + } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-002-ref.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-002-ref.html new file mode 100644 index 00000000000..8c297434751 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-002-ref.html @@ -0,0 +1,404 @@ + +<!DOCTYPE html> +<html> +<title>Scrollable Area with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box */ + .indicator { + width: 72px; + height: 72px; + background: green; + } + + /* Create a test box with appropriate scrollbars */ + .test { + width: 24px; + height: 24px; + overflow: hidden; + } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-block-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-block-001.html new file mode 100644 index 00000000000..20192eb2f10 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-block-001.html @@ -0,0 +1,502 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Block Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cl, + .center .no-scroll.ltr.vrl .tc, + .center .no-scroll.ltr.vlr .tc, + .center .no-scroll.rtl.htb .cr, + .center .no-scroll.rtl.vrl .bc, + .center .no-scroll.rtl.vlr .bc { background: green; } + + .end .no-scroll.ltr.htb .bl, + .end .no-scroll.ltr.vrl .tl, + .end .no-scroll.ltr.vlr .tr, + .end .no-scroll.rtl.htb .br, + .end .no-scroll.rtl.vrl .bl, + .end .no-scroll.rtl.vlr .br { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-block-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-block-002.html new file mode 100644 index 00000000000..587f7394449 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-block-002.html @@ -0,0 +1,479 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Block Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="start align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="start align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="start align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="start align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="start align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="start align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="start align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="start align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="start align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="start align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="start align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="start align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-001.html new file mode 100644 index 00000000000..30cb6c92db7 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-001.html @@ -0,0 +1,505 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Column Flex Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column-reverse wrap-reverse; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-002.html new file mode 100644 index 00000000000..a6252391c49 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-002.html @@ -0,0 +1,510 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Column Flex Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column-reverse wrap-reverse; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } + + .left .no-scroll.ltr.htb .tl, + .left .no-scroll.ltr.vrl .tl, + .left .no-scroll.ltr.vlr .tl, + .left .no-scroll.rtl.htb .tr, + .left .no-scroll.rtl.vrl .bl, + .left .no-scroll.rtl.vlr .bl { background: green; } + + .right .no-scroll.ltr.htb .tl, + .right .no-scroll.ltr.vrl .tr, + .right .no-scroll.ltr.vlr .tr, + .right .no-scroll.rtl.htb .tr, + .right .no-scroll.rtl.vrl .br, + .right .no-scroll.rtl.vlr .br { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="end align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="end align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="end align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="end align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="start align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="start align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-001.html new file mode 100644 index 00000000000..0a98dae690b --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-001.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Column Flex Containers with Overflow Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { xbackground: red; } /* Remove for debugging */ + + /* Create a test box containing a smaller box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column-reverse wrap-reverse; + } + .test > div { + width: 24px; + height: 24px; + flex: none; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-002.html new file mode 100644 index 00000000000..d5c842e4b70 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-reverse-overflow-002.html @@ -0,0 +1,489 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Column Flex Containers with Overflow Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column-reverse wrap-reverse; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-001.html new file mode 100644 index 00000000000..b980b94314c --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-001.html @@ -0,0 +1,505 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Column Flex Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column wrap; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-002.html new file mode 100644 index 00000000000..195a877ccd4 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-002.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Column Flex Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column wrap; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .left .no-scroll.ltr.htb .tl, + .left .no-scroll.ltr.vrl .tl, + .left .no-scroll.ltr.vlr .tl, + .left .no-scroll.rtl.htb .tr, + .left .no-scroll.rtl.vrl .bl, + .left .no-scroll.rtl.vlr .bl { background: green; } + + .right .no-scroll.ltr.htb .tl, + .right .no-scroll.ltr.vrl .tr, + .right .no-scroll.ltr.vlr .tr, + .right .no-scroll.rtl.htb .tr, + .right .no-scroll.rtl.vrl .br, + .right .no-scroll.rtl.vlr .br { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-001.html new file mode 100644 index 00000000000..59815fa0aa9 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-001.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Column Flex Containers with Overflow Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { xbackground: red; } /* Remove for debugging */ + + /* Create a test box containing a smaller box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column wrap; + } + .test > div { + width: 24px; + height: 24px; + flex: none; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-002.html new file mode 100644 index 00000000000..cb0ce12b9e9 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-col-wrap-overflow-002.html @@ -0,0 +1,489 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Column Flex Containers with Overflow Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: column wrap; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-001.html new file mode 100644 index 00000000000..c41e7ce3e52 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-001.html @@ -0,0 +1,505 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Row Flex Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row-reverse wrap-reverse; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-002.html new file mode 100644 index 00000000000..a032d59e675 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-002.html @@ -0,0 +1,510 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Row Flex Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row-reverse wrap-reverse; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } + + .left .no-scroll.ltr.htb .tl, + .left .no-scroll.ltr.vrl .tr, + .left .no-scroll.ltr.vlr .tl, + .left .no-scroll.rtl.htb .tl, + .left .no-scroll.rtl.vrl .tr, + .left .no-scroll.rtl.vlr .tl { background: green; } + + .right .no-scroll.ltr.htb .tr, + .right .no-scroll.ltr.vrl .br, + .right .no-scroll.ltr.vlr .bl, + .right .no-scroll.rtl.htb .tr, + .right .no-scroll.rtl.vrl .br, + .right .no-scroll.rtl.vlr .bl { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="end align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="end align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="end align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="end align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="end align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-001.html new file mode 100644 index 00000000000..fbc0ef02bf7 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-001.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Row Flex Containers with Overflow Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { xbackground: red; } /* Remove for debugging */ + + /* Create a test box containing a smaller box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row-reverse wrap-reverse; + } + .test > div { + width: 24px; + height: 24px; + flex: none; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-002.html new file mode 100644 index 00000000000..a1b9f4bae86 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-reverse-overflow-002.html @@ -0,0 +1,489 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Reversed Multiline Row Flex Containers with Overflow Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row-reverse wrap-reverse; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-001.html new file mode 100644 index 00000000000..51f02add772 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-001.html @@ -0,0 +1,505 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Row Flex Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row wrap; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-002.html new file mode 100644 index 00000000000..e5ee089d0c4 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-002.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Row Flex Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row wrap; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .left .no-scroll.ltr.htb .tl, + .left .no-scroll.ltr.vrl .tr, + .left .no-scroll.ltr.vlr .tl, + .left .no-scroll.rtl.htb .tl, + .left .no-scroll.rtl.vrl .tr, + .left .no-scroll.rtl.vlr .tl { background: green; } + + .right .no-scroll.ltr.htb .tr, + .right .no-scroll.ltr.vrl .br, + .right .no-scroll.ltr.vlr .bl, + .right .no-scroll.rtl.htb .tr, + .right .no-scroll.rtl.vrl .br, + .right .no-scroll.rtl.vlr .bl { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-001.html new file mode 100644 index 00000000000..63f5ef2c8ea --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-001.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Row Flex Containers with Overflow Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { xbackground: red; } /* Remove for debugging */ + + /* Create a test box containing a smaller box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row wrap; + } + .test > div { + width: 24px; + height: 24px; + flex: none; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + start + + + <tr class="align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe center + + + <tr class="align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + safe end + + <tr class="align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + center + + + <tr class="align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + end + + <tr class="align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe center + + <tr class="align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="container"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-002.html new file mode 100644 index 00000000000..e1d3fb2c7d9 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-flex-row-wrap-overflow-002.html @@ -0,0 +1,489 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Multiline Row Flex Containers with Overflow Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + flex: none; + margin: -24px; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: flex; + flex-flow: row wrap; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .no-scroll .cc { background: green; } + + .scroll-TL.ltr.htb .cc { background: green; } + .scroll-TL.ltr.vrl .cl { background: green; } + .scroll-TL.ltr.vlr .cc { background: green; } + .scroll-TL.rtl.htb .cl { background: green; } + .scroll-TL.rtl.vrl .tl { background: green; } + .scroll-TL.rtl.vlr .tc { background: green; } + + .scroll-BR.ltr.htb .br { background: green; } + .scroll-BR.ltr.vrl .bc { background: green; } + .scroll-BR.ltr.vlr .br { background: green; } + .scroll-BR.rtl.htb .bc { background: green; } + .scroll-BR.rtl.vrl .cc { background: green; } + .scroll-BR.rtl.vlr .cr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-001.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-001.html new file mode 100644 index 00000000000..bf98b62116d --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-001.html @@ -0,0 +1,503 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Grid Containers with Content Alignment Start/Center/End</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: grid; + } + .align-start .test { place-content: start; } + .align-center .test { place-content: center; } + .align-end .test { place-content: end; } + .align-safe-center .test { place-content: safe center; } + .align-safe-end .test { place-content: safe end; } + .align-unsafe-center .test { place-content: unsafe center; } + .align-unsafe-end .test { place-content: unsafe end; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .end .no-scroll.ltr.htb .br, + .end .no-scroll.ltr.vrl .bl, + .end .no-scroll.ltr.vlr .br, + .end .no-scroll.rtl.htb .bl, + .end .no-scroll.rtl.vrl .tl, + .end .no-scroll.rtl.vlr .tr { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-start"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-start"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + start + + + <tr class="start align-safe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe center + + + <tr class="start align-safe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + safe end + + <tr class="center align-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + center + + + <tr class="end align-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + end + + <tr class="center align-unsafe-center"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe center + + <tr class="end align-unsafe-end"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + unsafe end +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-002.html b/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-002.html new file mode 100644 index 00000000000..76d3886d7c6 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/overflow-alignment-grid-002.html @@ -0,0 +1,501 @@ + +<!DOCTYPE html> +<!-- No, you should not convert this test to testharness.js. --> +<html class="reftest-wait"> +<title>Scrollable Area of Grid Containers with Content Alignment Miscellaneous</title> +<link rel="help" href="https://www.w3.org/TR/css-align/#overflow-scroll-position"> +<link rel="help" href="https://www.w3.org/TR/css-overflow/#scrollable"> +<link rel="help" href="https://www.w3.org/TR/css-writing-modes/"> +<link rel="author" href="http://fantasai.inkedblade.net/contact" title="Elika J. Etemad"> + +<style> + /* Cram Tests */ + body { height: 600px; border-bottom: solid orange; } /* Reftest Max Size. Do not exceed this line. */ + html { font-size: 10px; } + th, td { padding: 0; } + + /* Styling/Readability */ + abbr, th[scope=row] { font-variant: small-caps; text-transform: lowercase; color: gray; } + thead { display: table-footer-group; } + caption { font-weight: bold; caption-side: bottom; } + /* Note: Annotations are at the bottom / right to avoid using up checked reftest area. */ + + /* Create an overflowing box with a 9-grid of colors */ + .indicator { + width: 72px; + height: 72px; + writing-mode: horizontal-tb; + direction: ltr; + } + .indicator > div { + width: 24px; + height: 24px; + } + .indicator > .tl { background: teal; float: left; } + .indicator > .tc { background: lightblue; float: left; } + .indicator > .tr { background: aqua; float: right; } + .indicator > .cl { background: gold; float: left; clear: both; } + .indicator > .cc { background: orange; float: left; } + .indicator > .cr { background: yellow; float: right; } + .indicator > .bl { background: fuchsia; float: left; clear: both; } + .indicator > .bc { background: thistle; float: left; } + .indicator > .br { background: purple; float: right; } + .indicator > [class] { background: red; } /* Remove for debugging */ + + /* Create a test box containing the overflowing indicator */ + .test { /* Expand for debugging */ + width: 24px; + height: 24px; + overflow: scroll; + display: grid; + } + .align-normal .test { place-content: normal; } + .align-stretch .test { place-content: stretch; } + .align-left .test { place-content: start left; } + .align-right .test { place-content: start right; } + .align-space-around .test { place-content: space-around; } + .align-space-evenly .test { place-content: space-evenly; } + .align-space-between .test { place-content: space-between; } + .ltr { direction: ltr; } + .rtl { direction: rtl; } + .htb { writing-mode: horizontal-tb; } + .vrl { writing-mode: vertical-rl; } + .vlr { writing-mode: vertical-lr; } + .no-scroll { overflow: hidden; } + + /* Pass Conditions */ /* Remove for debugging */ + .scroll-TL .tl { background: green; } + .scroll-BR .br { background: green; } + + .start .no-scroll.ltr.htb .tl, + .start .no-scroll.ltr.vrl .tr, + .start .no-scroll.ltr.vlr .tl, + .start .no-scroll.rtl.htb .tr, + .start .no-scroll.rtl.vrl .br, + .start .no-scroll.rtl.vlr .bl { background: green; } + + .center .no-scroll.ltr.htb .cc, + .center .no-scroll.ltr.vrl .cc, + .center .no-scroll.ltr.vlr .cc, + .center .no-scroll.rtl.htb .cc, + .center .no-scroll.rtl.vrl .cc, + .center .no-scroll.rtl.vlr .cc { background: green; } + + .left .no-scroll.ltr.htb .tl, + .left .no-scroll.ltr.vrl .tr, + .left .no-scroll.ltr.vlr .tl, + .left .no-scroll.rtl.htb .tl, + .left .no-scroll.rtl.vrl .tr, + .left .no-scroll.rtl.vlr .tl { background: green; } + + .right .no-scroll.ltr.htb .tr, + .right .no-scroll.ltr.vrl .br, + .right .no-scroll.ltr.vlr .bl, + .right .no-scroll.rtl.htb .tr, + .right .no-scroll.rtl.vrl .br, + .right .no-scroll.rtl.vlr .bl { background: green; } +</style> + +<table> + <caption>Each box must be completely green.</caption> +<thead> + <tr> + <th colspan=3><abbr title="direction: ltr">LTR</abbr> + <th colspan=3><abbr title="direction: rtl">RTL</abbr> + <tr> + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + + <th><abbr title="writing-mode: horizontal-tb">HTB</abbr> + <th><abbr title="writing-mode: vertical-rl">VRL</abbr> + <th><abbr title="writing-mode: vertical-lr">VLR</abbr> + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Do not scroll any box below. + + <tr class="start align-normal"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr no-scroll"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the top left. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-TL"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between + + +<tbody> + <tr> + <th colspan=6 scope=rowgroup>Scroll each box below to the bottom right. + <th> + + <tr class="start align-normal"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + normal + + <tr class="start align-stretch"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + stretch + + <tr class="left align-left"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + left + + <tr class="right align-right"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + right + + <tr class="center align-space-around"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-around + + <tr class="center align-space-evenly"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-evenly + + <tr class="start align-space-between"> + <td> + <div class="test ltr htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test ltr vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl htb scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vrl scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <td> + <div class="test rtl vlr scroll-BR"><div class="indicator"><div class=tl></div><div class=tc></div><div class=tr></div><div class=cl></div><div class=cc></div><div class=cr></div><div class=bl></div><div class=bc></div><div class=br></div></div></div> + <th scope=row> + space-between +</table> + +<script> + function test(isReftest) + { + // Simplify reftest reference by removing scrollbars + if (isReftest) { + scrollers = document.getElementsByClassName('test'); + for (let s of scrollers) { + s.style.overflow = "hidden"; + } + } + + // Trigger layout + document.body.offsetHeight; + + // Scroll to the top left + var scrollers = document.getElementsByClassName('scroll-TL'); + for (let s of scrollers) { + s.scrollTop = -1000; + s.scrollLeft = -1000; + } + + // Scroll to the bottom right + scrollers = document.getElementsByClassName('scroll-BR'); + for (let s of scrollers) { + s.scrollTop = 1000; + s.scrollLeft = 1000; + } + + document.body.offsetHeight; // trigger layout + + document.documentElement.removeAttribute("class"); + }; + document.addEventListener("TestRendered", function(){ test(true); }); + window.addEventListener("load", function(){ test(false); }); // for manual inspection +</script> diff --git a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-043.html b/tests/wpt/tests/css/css-overflow/webkit-line-clamp-043.html deleted file mode 100644 index 54f0ac538da..00000000000 --- a/tests/wpt/tests/css/css-overflow/webkit-line-clamp-043.html +++ /dev/null @@ -1,23 +0,0 @@ -<!DOCTYPE html> -<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org"> -<link rel="help" href="https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp"> -<link rel="match" href="../reference/ref-filled-green-100px-square.xht"> -<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> -<div style="columns:3; column-fill:auto; gap:0; width:300px; height:250px;"> - <div style="display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:clip; line-height:50px; color:transparent; background:green;"> - <br> - <br> - <span style="color:red;"> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - FAIL<br> - </span> - </div> -</div> diff --git a/tests/wpt/tests/css/css-page/page-background-image-print-ref.html b/tests/wpt/tests/css/css-page/page-background-image-print-ref.html new file mode 100644 index 00000000000..f38cc898380 --- /dev/null +++ b/tests/wpt/tests/css/css-page/page-background-image-print-ref.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> + +<style> + html { + background-color: lime; + } + + @page { + margin: 0px; + } +</style> + +<p> + Should print on a green background but not display it on screen. +</p> diff --git a/tests/wpt/tests/css/css-page/page-background-image-print.html b/tests/wpt/tests/css/css-page/page-background-image-print.html new file mode 100644 index 00000000000..633cd4ec6c9 --- /dev/null +++ b/tests/wpt/tests/css/css-page/page-background-image-print.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>Test print result of background-image only for printed page</title> + +<link rel="help" href="https://drafts.csswg.org/css-page-3/#page-properties"> +<link rel="help" href="https://crbug.com/341947679"> + +<link rel="match" href="page-background-image-print-ref.html"> + +<style> + @page { + background-image: url("/images/green.png"); + margin: 0px; + } +</style> + +<p> + Should print on a green background but not display it on screen. +</p> diff --git a/tests/wpt/tests/css/css-pseudo/first-letter-bidi-pre-crash.html b/tests/wpt/tests/css/css-pseudo/first-letter-bidi-pre-crash.html new file mode 100644 index 00000000000..fbff1c8f0bc --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/first-letter-bidi-pre-crash.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<meta charset=windows-1252> +<style> + * { + float: inline-end; + } + + *::first-letter { + } +</style> +<pre> +<strong dir="rtl"> +٪9<
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-pseudo/first-letter-width-2-ref.html b/tests/wpt/tests/css/css-pseudo/first-letter-width-2-ref.html new file mode 100644 index 00000000000..67fe00ca790 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/first-letter-width-2-ref.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html lang="en"> +<head> + <title>initial-letter width test</title> + <style> + body { + overflow: scroll; + } + span { + position: absolute; + color: transparent; + } + #control { + background-color: green; + } + </style> +</head> +<body> + <p>There should be no red</p> + <span id="control">Test.</span> +</body> +</html> diff --git a/tests/wpt/tests/css/css-pseudo/first-letter-width-2.html b/tests/wpt/tests/css/css-pseudo/first-letter-width-2.html new file mode 100644 index 00000000000..e7d01afe472 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/first-letter-width-2.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html lang="en"> +<head> + <title>initial-letter width test</title> + <link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=362880"> + <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#first-letter-styling"> + <link rel="match" href="first-letter-width-2-ref.html"> + <meta name="assert" content="The width of an element with first-letter styling should render correctly."> + <style> + html { + overflow: scroll; + } + body { + overflow: scroll; + } + div { + position: relative; + } + span { + position: absolute; + color: transparent; + } + #control { + background-color: green; + } + #test { + background-color: red; + line-height: 1; + top: 1px; + } + #test::first-letter { + text-transform: uppercase; + } + </style> + <script> + function run() { + document.documentElement.style.overflow = "initial"; + } + </script> +</head> +<body onload="run()"> + <p>There should be no red</p> + <div> + <span id="test">Test</span> + <span id="control">Test.</span> + </div> +</body> +</html> diff --git a/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box-ref.html b/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box-ref.html new file mode 100644 index 00000000000..c9839d94cf2 --- /dev/null +++ b/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box-ref.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<body> + +<div> +<ruby>base<rt>annotation</ruby> +</div> + +<div> +<ruby>base<rt>annotation</ruby> +</div> + +<div> +<ruby>base<rt>annotation</ruby> +</div> +</body> diff --git a/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box.html b/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box.html new file mode 100644 index 00000000000..cf9bb43826e --- /dev/null +++ b/tests/wpt/tests/css/css-ruby/interlinear-block-margin-box.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/css-ruby/#interlinear-block"> +<link rel="mismatch" href="interlinear-block-margin-box-ref.html"> +<title>padding/border/margin should affect block-axis positioning</title> +<body> + +<div> +<ruby>base<rt style="padding-bottom: 5px;">annotation</ruby> +</div> + +<div> +<ruby>base<rt style="border-bottom: 7px solid transparent">annotation</ruby> +</div> + +<div> +<ruby>base<rt style="margin-bottom: 11px">annotation</ruby> +</div> +</body> diff --git a/tests/wpt/tests/css/css-ruby/pseudo-first-letter-ref.html b/tests/wpt/tests/css/css-ruby/pseudo-first-letter-ref.html new file mode 100644 index 00000000000..3834976b813 --- /dev/null +++ b/tests/wpt/tests/css/css-ruby/pseudo-first-letter-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<style> +span { + color: green; +} +</style> +<div><ruby><span>B</span>ase<rt>Annotation</ruby></div> diff --git a/tests/wpt/tests/css/css-ruby/pseudo-first-letter.html b/tests/wpt/tests/css/css-ruby/pseudo-first-letter.html new file mode 100644 index 00000000000..dcb9e8583b4 --- /dev/null +++ b/tests/wpt/tests/css/css-ruby/pseudo-first-letter.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/css-pseudo/#first-letter-application"> +<link rel="match" href="pseudo-first-letter-ref.html"> +<style> +div::first-letter { + color: green; +} +</style> +<div><ruby>Base<rt>Annotation</ruby></div> diff --git a/tests/wpt/tests/css/css-syntax/serialize-escape-identifiers.html b/tests/wpt/tests/css/css-syntax/serialize-escape-identifiers.html new file mode 100644 index 00000000000..90476d6eca0 --- /dev/null +++ b/tests/wpt/tests/css/css-syntax/serialize-escape-identifiers.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> + <head> + <title>Properly escape CSS identifiers</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <link rel="help" href="https://crbug.com/343000522"> + <style id="sheet"> + @import 'abc' layer(\{\}); + @counter-style abc\{\}oops {} + @font-feature-values abc\{\}oops {} + @font-palette-values --abc\{\}oops {} + @keyframes abc\{\}oops {} + @layer abc\;oops\!; + </style> + </head> + <body> + <script> + test(()=>{ + const rules = Array.from(document.styleSheets[0].cssRules); + const text = rules.map(r => r.cssText).join('\n'); + sheet.innerText = text; + + const new_rules = Array.from(document.styleSheets[0].cssRules); + const new_text = new_rules.map(r => r.cssText).join('\n'); + assert_equals(new_text, text); + }, 'Rules must be the same after serialization round-trip, even with escaped characters'); + </script> diff --git a/tests/wpt/tests/css/css-transforms/transform-with-sign-function.html b/tests/wpt/tests/css/css-transforms/transform-with-sign-function.html index 64b30dca17e..96ba054fbef 100644 --- a/tests/wpt/tests/css/css-transforms/transform-with-sign-function.html +++ b/tests/wpt/tests/css/css-transforms/transform-with-sign-function.html @@ -22,5 +22,6 @@ test_math_used('rotateX(calc(sign(1em - 1px) * 2deg))', 'rotateX(2deg)', {prop:' test_math_used('rotateY(calc(sign(1em - 1px) * 2deg))', 'rotateY(2deg)', {prop:'transform'}); test_math_used('rotateZ(calc(sign(1em - 1px) * 2deg))', 'rotateZ(2deg)', {prop:'transform'}); test_math_used('calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2)', '2 2 2', {prop:'scale'}); +test_math_used('calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2) calc(sign(1em - 1px) * 2deg)', '2 2 2 2deg', {prop:'rotate'}); </script> diff --git a/tests/wpt/tests/css/css-transitions/starting-style-cascade.html b/tests/wpt/tests/css/css-transitions/starting-style-cascade.html index cef3e88b656..921ba6b2004 100644 --- a/tests/wpt/tests/css/css-transitions/starting-style-cascade.html +++ b/tests/wpt/tests/css/css-transitions/starting-style-cascade.html @@ -40,6 +40,16 @@ @starting-style { #t5 > div { color: inherit; } } + + #t6 { color: green; } + @starting-style { + #t6 { color: black; } + } + #t6 .color-transition { color: lime; } + @starting-style { + #t6 .color-transition { color: inherit; } + } + </style> <div id="t1" hidden class="color-transition"></div> <div id="t2" hidden class="color-transition"></div> @@ -52,6 +62,11 @@ <div id="t5" hidden class="color-transition"> <div class="color-transition"></div> </div> +<div id="t6" hidden class="color-transition"> + <div> + <div class="color-transition"></div> + </div> +</div> <script> setup(() => { assert_true(supportsStartingStyle(), "Prerequisite: @starting-style parses"); @@ -98,4 +113,16 @@ assert_equals(getComputedStyle(t5.firstElementChild).color, "rgb(0, 192, 0)", "Transition started from parent's after-change style color"); }, "Starting style inheriting from parent's after-change style while parent transitioning"); + + promise_test(async t => { + t6.removeAttribute("hidden"); + await waitForAnimationFrames(2); + assert_equals(getComputedStyle(t6).color, "rgb(0, 64, 0)", + "Parent transition started"); + assert_equals(getComputedStyle(t6.firstElementChild).color, "rgb(0, 64, 0)", + "Inherited effect from parent transition"); + assert_equals(getComputedStyle(t6.firstElementChild.firstElementChild).color, "rgb(0, 192, 0)", + "Transition started from parent's after-change style color, inherited from ancestors after-change color"); + }, "Starting style inheriting from parent's after-change style while ancestor is transitioning"); + </script> diff --git a/tests/wpt/tests/css/css-ui/box-sizing-007.html b/tests/wpt/tests/css/css-ui/box-sizing-007.html index c51f4318db4..8e3c5a63255 100644 --- a/tests/wpt/tests/css/css-ui/box-sizing-007.html +++ b/tests/wpt/tests/css/css-ui/box-sizing-007.html @@ -7,7 +7,7 @@ <link rel="help" href="http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height"> <meta name="flags" content="svg"> <link rel="match" href="reference/box-sizing-007-ref.html"> -<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with either both intrisic dimentions or an intrinsic ratio, to check that they work correctly in terms of the content width height."> +<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with either both intrinsic dimentions or an intrinsic ratio, to check that they work correctly in terms of the content width height."> <style> img { box-sizing: border-box; diff --git a/tests/wpt/tests/css/css-ui/box-sizing-008.html b/tests/wpt/tests/css/css-ui/box-sizing-008.html index 3e79e29bcdc..f4409603899 100644 --- a/tests/wpt/tests/css/css-ui/box-sizing-008.html +++ b/tests/wpt/tests/css/css-ui/box-sizing-008.html @@ -7,7 +7,7 @@ <link rel="help" href="http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height"> <meta name="flags" content="svg"> <link rel="match" href="reference/box-sizing-008-ref.html"> -<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with intrisic width and no intrinsic ratio, to check that they work correctly in terms of the content width height."> +<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with intrinsic width and no intrinsic ratio, to check that they work correctly in terms of the content width height."> <style> #ref { width: 100px; diff --git a/tests/wpt/tests/css/css-ui/box-sizing-009.html b/tests/wpt/tests/css/css-ui/box-sizing-009.html index 69d526d63d3..b13b64c0f83 100644 --- a/tests/wpt/tests/css/css-ui/box-sizing-009.html +++ b/tests/wpt/tests/css/css-ui/box-sizing-009.html @@ -7,7 +7,7 @@ <link rel="help" href="http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height"> <meta name="flags" content="svg"> <link rel="match" href="reference/box-sizing-009-ref.html"> -<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with intrisic height and no intrinsic ratio, to check that they work correctly in terms of the content width height."> +<meta name="assert" content="Exercises the sizing rules in CSS2.1 10.3.2 and 10.6.2 with box-sizing:border-box for replaced elements with intrinsic height and no intrinsic ratio, to check that they work correctly in terms of the content width height."> <style> #ref { width: 300px; diff --git a/tests/wpt/tests/css/css-values/rlh-unit-001.html b/tests/wpt/tests/css/css-values/rlh-unit-001.html new file mode 100644 index 00000000000..c435fd99709 --- /dev/null +++ b/tests/wpt/tests/css/css-values/rlh-unit-001.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Values and Units Test: using rlh in calc</title> +<link rel="help" href="https://drafts.csswg.org/css-values-4/#font-relative-lengths"> +<link rel="match" href="../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="The rlh unit resolves against the root when used in calc."> +<style> + :root { + line-height: 50px; + } + + p { + line-height: normal; + } + + .container { + position: relative; + width: 100px; + height: 100px; + } + + .contents { + position: absolute; + top: 0; + left: 0; + } + + .div1 { + background-color: green; + width: 100px; + height: 100px; + } + + .div2 { + line-height: 10px; + block-size: 100px; + background-color: red; + inline-size: calc(1rlh - 1rlh); + } +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> + +<div class="container"> + <div class="div1 contents"></div> + <div class="div2 contents"></div> +</div> diff --git a/tests/wpt/tests/css/css-view-transitions/view-transition-types-universal-match.html b/tests/wpt/tests/css/css-view-transitions/active-view-transition-pseudo-class-match.html index 534c144ecf0..534c144ecf0 100644 --- a/tests/wpt/tests/css/css-view-transitions/view-transition-types-universal-match.html +++ b/tests/wpt/tests/css/css-view-transitions/active-view-transition-pseudo-class-match.html diff --git a/tests/wpt/tests/css/css-view-transitions/inline-with-offset-from-containing-block-clipped-ref.html b/tests/wpt/tests/css/css-view-transitions/inline-with-offset-from-containing-block-clipped-ref.html new file mode 100644 index 00000000000..2b6b56578c2 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/inline-with-offset-from-containing-block-clipped-ref.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<title>View transitions: capture elements with display inline (ref)</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<style> +html { + background: pink; +} +#box { + will-change: opacity; + background: blue; +} + +div { + padding-left: 8px; + padding-top: 8px; +} +</style> + +<div> + <span id=box> </span> +</div> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2-ref.html b/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2-ref.html new file mode 100644 index 00000000000..f37c9544be6 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2-ref.html @@ -0,0 +1,22 @@ +<!doctype HTML> +<html> +<head> +<style> +html { + background: lightpink; +} +div { + position: relative; + width: 200px; + height: 200px; + background-color: red; + left: 150px; + top: 150px; +} + +</style> +</head> +<body> +<div></div> +</body> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2.html b/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2.html new file mode 100644 index 00000000000..3ce02097476 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-ancestor-clipped-2.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>View transitions: clips from ancestor elements are not applied to captures</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="new-content-ancestor-clipped-2-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> +.outer { + background-color: blue; + overflow: hidden; + position: relative; + left: 100px; + top: 100px; + width: 200px; + height: 200px; +} +.inner { + background-color: red; + position: relative; + left: 50px; + top: 50px; + width: 200px; + height: 200px; + view-transition-name: inner; +} +/* We're verifying what we capture, so just display the new contents 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: 0; } +/* 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: lightpink; } +</style> +<div class="outer"> + <div class="inner"></div> +</div> +<script> +async function runTest() { + let t = document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> + diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor-ref.html b/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor-ref.html new file mode 100644 index 00000000000..8728d34ae49 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor-ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<title>View transitions: computed transform for elements with transform-style:flat ancestors is correct (ref)</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<style> +body { + perspective: 1000px; +} +.box { + background: lightblue; + width: 100px; + height: 100px; + transform: rotateY(60deg); +} +.outer { + width: 100px; + height: 100px; + transform: rotateY(60deg); + perspective: 1000px; +} +body { background: lightpink; } +</style> +<div class=outer> + <div class=outer> + <div class=box></div> + </div> +</div> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor.html b/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor.html new file mode 100644 index 00000000000..112a0af2c45 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-flat-transform-ancestor.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>View transitions: computed transform for elements with transform-style:flat ancestors is correct</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" title="Matt Woodrow" href="mailto:mattwoodrow@apple.com"> +<link rel="match" href="new-content-flat-transform-ancestor-ref.html"> +<meta name="fuzzy" content="maxDifference=0-63; totalPixels=0-510" /> +<script src="/common/rendering-utils.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="../../../../../resources/ui-helper.js"></script> +<style> +body { + perspective: 1000px; +} +.box { + background: lightblue; + width: 100px; + height: 100px; + transform: rotateY(60deg); + view-transition-name: target; +} +.outer { + width: 100px; + height: 100px; + transform: rotateY(60deg); + perspective: 1000px; +} + +/* We're verifying what we capture, so just display the new contents 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: 0; } +/* 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: lightpink; } +</style> +<div class=outer> + <div class=outer> + <div class=box></div> + </div> +</div> +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + await waitForAtLeastOneFrame(); + + let t = document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-inline-with-offset-from-containing-block-clipped.html b/tests/wpt/tests/css/css-view-transitions/new-content-inline-with-offset-from-containing-block-clipped.html new file mode 100644 index 00000000000..2b122d0a746 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-inline-with-offset-from-containing-block-clipped.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>View transitions: capture elements with display inline</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="inline-with-offset-from-containing-block-clipped-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> +#box { + background: blue; + view-transition-name: target; +} + +div { + padding-left: 8px; + padding-top: 8px; + background-color: green; +} + +/* We're verifying what we capture, so just display the contents for 5 minutes. */ +html::view-transition-group(*) { animation-duration: 300s; } +html::view-transition-group(target) { background: green; } +html::view-transition-new(*) { animation: unset; opacity: 1; } +html::view-transition-old(*) { animation: unset; opacity: 0; } +/* 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; } + +::view-transition-image-pair(target) { + overflow:clip; +} +</style> + +<div> + <span id=box>   </span> +</div> +<script> + +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/new-content-preserve-3d-ancestor-ref.html b/tests/wpt/tests/css/css-view-transitions/new-content-preserve-3d-ancestor-ref.html new file mode 100644 index 00000000000..c6c054c130b --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-preserve-3d-ancestor-ref.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<title>View transitions: computed transform for elements with transform-style:preserve-3d ancestors is correct (ref)</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<style> +.box { + background: lightblue; + width: 100px; + height: 100px; + position: fixed; +} +body { background: lightpink; } +</style> +<div class=box></div> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-preserve-3d-ancestor.html b/tests/wpt/tests/css/css-view-transitions/new-content-preserve-3d-ancestor.html new file mode 100644 index 00000000000..61f274d276c --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-preserve-3d-ancestor.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>View transitions: computed transform for elements with transform-style:preserve-3d ancestors is correct</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" title="Matt Woodrow" href="mailto:mattwoodrow@apple.com"> +<link rel="match" href="new-content-preserve-3d-ancestor-ref.html"> +<script src="/common/rendering-utils.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="../../../../../resources/ui-helper.js"></script> +<style> +.box { + background: lightblue; + width: 100px; + height: 100px; + transform: rotateY(60deg); + view-transition-name: target; +} +.outer { + width: 100px; + height: 100px; + transform-style: preserve-3d; + transform: rotateY(60deg); +} + +/* We're verifying what we capture, so just display the new contents 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: 0; } +/* 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: lightpink; } +</style> +<div class=outer> + <div class=outer> + <div class=box></div> + </div> +</div> +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + await waitForAtLeastOneFrame(); + + let t = document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-root-scrollbar-with-fixed-background.html b/tests/wpt/tests/css/css-view-transitions/new-content-root-scrollbar-with-fixed-background.html new file mode 100644 index 00000000000..89f8158b58b --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-root-scrollbar-with-fixed-background.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html class=reftest-wait style="background: lightblue;"> +<title>When the root element has scrollbars, these should be excluded in new snapshot</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" href="mailto:khushalsagar@chromium.org"> +<link rel="match" href="root-scrollbar-with-fixed-background-ref.html"> +<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-4500"> + +<script src="/common/rendering-utils.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> +#hide { + position: absolute; + top: 0px; + left: 0px; + width: 10px; + height: 10px; + background: red; + contain: paint; + view-transition-name: hide; +} +#first { + width: 10px; + background: linear-gradient(green, blue); + height: 1000px; +} +body { + margin: 0px; + padding: 0px; +} + +/* Set a no-op animation to screenshot the pseudo transition DOM. */ +html::view-transition-group(hide) { + animation-duration: 300s; + opacity: 0; +} +html::view-transition-new(*) { + animation: unset; + filter: invert(1); + height: 100%; +} +html::view-transition-old(*) { + animation: unset; + opacity: 0; + height: 100%; +} +</style> + +<div id=hide></div> +<div id=first></div> + +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + await waitForAtLeastOneFrame(); + + await new Promise((resolve) => { + addEventListener("scroll", () => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + }, { once: true, capture: true }); + + document.documentElement.scrollTop = 500; + }); + document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} + +onload = () => requestAnimationFrame(runTest); +</script> + +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed-ref.html b/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed-ref.html new file mode 100644 index 00000000000..3c3a33d0b2c --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed-ref.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>View transitions: computed transform for position fixed elements doesn't include scroll pos (ref)</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<style> +.box { + background: lightblue; + width: 100px; + height: 100px; + position: fixed; +} +body { background: lightpink; } +</style> +<div class=box></div> +<div style="height: 1000px;"></div> +<script> + addEventListener("scroll", () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + document.documentElement.classList.remove("reftest-wait"); + }); + }); + }, { once: true, capture: true }); + document.documentElement.scrollTop = 500; +</script> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed.html b/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed.html new file mode 100644 index 00000000000..4b27796c9b4 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/new-content-transform-position-fixed.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>View transitions: computed transform for position fixed elements doesn't include scroll pos</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" title="Matt Woodrow" href="mailto:mattwoodrow@apple.com"> +<link rel="match" href="new-content-transform-position-fixed-ref.html"> +<script src="/common/rendering-utils.js"></script> +<script src="/common/reftest-wait.js"></script> +<script src="../../../../../resources/ui-helper.js"></script> +<style> +.box { + background: lightblue; + width: 100px; + height: 100px; + position: fixed; + view-transition-name: target; +} + +/* We're verifying what we capture, so just display the new contents 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: 0; } +/* 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: lightpink; } +</style> +<div class=box></div> +<div style="height: 1000px;"></div> +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + await waitForAtLeastOneFrame(); + + /* Scroll the doc, target element's element-to-screen transform should not change */ + await new Promise((resolve) => { + addEventListener("scroll", () => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + }, { once: true, capture: true }); + + document.documentElement.scrollTop = 500; + }); + let t = document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> diff --git a/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked-ref.html b/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked-ref.html new file mode 100644 index 00000000000..8c7ab1bc5ff --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<title>View transitions: Rendering suppression prevents painting (ref)</title> +<link rel="author" href="mailto:mattwoodrow@apple.com"> +<style> +#target { + width: 200px; + height: 200px; + background: green; +} +</style> + +<div id=target></div> diff --git a/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked.html b/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked.html new file mode 100644 index 00000000000..1feb4759eb8 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/no-painting-while-render-blocked.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html class=reftest-wait> +<title>View transitions: Rendering suppression prevents painting</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/#document-rendering-suppression-for-view-transitions"> +<link rel="author" href="mailto:mattwoodrow@apple.com"> +<link rel="match" href="no-painting-while-render-blocked-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> +#target { + width: 200px; + height: 200px; + background: green; +} +</style> + +<div id=target></div> + +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + let transition = document.startViewTransition(async () => { + document.getElementById('target').style.backgroundColor = "red"; + takeScreenshot(); + await new Promise(resolve => setTimeout(resolve, 5000)); + }); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> + + diff --git a/tests/wpt/tests/css/css-view-transitions/old-content-inline-with-offset-from-containing-block-clipped.html b/tests/wpt/tests/css/css-view-transitions/old-content-inline-with-offset-from-containing-block-clipped.html new file mode 100644 index 00000000000..7f35d8599b3 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/old-content-inline-with-offset-from-containing-block-clipped.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>View transitions: capture elements with display inline</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="inline-with-offset-from-containing-block-clipped-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> +#box { + background: blue; + view-transition-name: target; +} + +div { + padding-left: 8px; + padding-top: 8px; + background-color: green; +} + +/* We're verifying what we capture, so just display the contents for 5 minutes. */ +html::view-transition-group(*) { animation-duration: 300s; } +html::view-transition-group(target) { background: green; } +html::view-transition-new(*) { animation: unset; opacity: 0; } +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; } + +::view-transition-image-pair(target) { + overflow:clip; +} +</style> + +<div> + <span id=box>   </span> +</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/old-content-root-scrollbar-with-fixed-background.html b/tests/wpt/tests/css/css-view-transitions/old-content-root-scrollbar-with-fixed-background.html new file mode 100644 index 00000000000..7c8b7dd92e4 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/old-content-root-scrollbar-with-fixed-background.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html class=reftest-wait style="background: lightblue;"> +<title>When the root element has scrollbars, these should be excluded in old snapshot</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" href="mailto:khushalsagar@chromium.org"> +<link rel="match" href="root-scrollbar-with-fixed-background-ref.html"> +<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-4500"> + +<script src="/common/rendering-utils.js"></script> +<script src="/common/reftest-wait.js"></script> + +<style> +#hide { + position: absolute; + top: 0px; + left: 0px; + width: 10px; + height: 10px; + background: red; + contain: paint; + view-transition-name: hide; +} +#first { + width: 10px; + background: linear-gradient(green, blue); + height: 1000px; +} +body { + margin: 0px; + padding: 0px; +} + +/* Set a no-op animation to screenshot the pseudo transition DOM. */ +html::view-transition-group(hide) { + animation-duration: 300s; + opacity: 0; +} +html::view-transition-new(*) { + animation: unset; + opacity: 0; + height: 100%; +} +html::view-transition-old(*) { + animation: unset; + filter: invert(1); + height: 100%; +} +</style> + +<div id=hide></div> +<div id=first></div> + +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + await waitForAtLeastOneFrame(); + + await new Promise((resolve) => { + addEventListener("scroll", () => { + requestAnimationFrame(() => requestAnimationFrame(resolve)); + }, { once: true, capture: true }); + + document.documentElement.scrollTop = 500; + }); + document.startViewTransition(() => { + requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); + }); +} + +onload = () => requestAnimationFrame(runTest); +</script> + +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition-ref.html b/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition-ref.html new file mode 100644 index 00000000000..93cb9975d4c --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition-ref.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <title>Reference: Finishing a View Transition on a scrolled page should properly reset state</title> + <style> + html { + background: lightblue; + } + body { + background-color: lightgreen; + } + </style> +</head> +<body> + <p>Start</p> + <div style="height: 200vh"></div> + <p>End</p> + + <script> + function scrollBy(y) { + return new Promise(resolve => { + addEventListener("scroll", () => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }, { once: true, capture: true }); + document.documentElement.scrollBy({ + top: y, + behavior: "instant" + }); + }); + } + addEventListener("load", async () => { + await scrollBy(document.documentElement.scrollHeight); + document.documentElement.classList.remove("reftest-wait"); + }); + </script> +</body> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition.html b/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition.html new file mode 100644 index 00000000000..5f8c14fd9b7 --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/reset-state-after-scrolled-view-transition.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <title>Finishing a View Transition on a scrolled page should properly reset state</title> + <link rel="author" title="Tim Nguyen" href="https://github.com/nt1m"> + <link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> + <link rel="match" href="reset-state-after-scrolled-view-transition-ref.html"> + <style> + html { + background: lightblue; + } + body { + background-color: lightgreen; + } + ::view-transition-group(*) { + animation-duration: 2s; + } + </style> +</head> +<body> + <p>Start</p> + <div id="block" style="height: 150vh"></div> + <p>End</p> + + <script> + function scrollBy(y) { + return new Promise(resolve => { + addEventListener("scroll", () => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + }, { once: true, capture: true }); + document.documentElement.scrollBy({ + top: y, + behavior: "instant" + }); + }); + } + addEventListener("load", async () => { + await scrollBy(document.documentElement.scrollHeight / 2); + const transition = document.startViewTransition(() => { block.style.height = '200vh' }); + await transition.ready; + scrollBy(document.documentElement.scrollHeight / 2); + await transition.finished; + document.documentElement.classList.remove("reftest-wait"); + }); + </script> +</body> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html b/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html index 7b2a6103f3b..ae1ff38f2f6 100644 --- a/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html +++ b/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background-ref.html @@ -25,6 +25,13 @@ body { <script> function scrollContainer() { + addEventListener("scroll", () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + document.documentElement.classList.remove("reftest-wait"); + }); + }); + }, { once: true, capture: true }); document.documentElement.scrollTop = 500; } diff --git a/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html b/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html deleted file mode 100644 index 3c3429412bf..00000000000 --- a/tests/wpt/tests/css/css-view-transitions/root-scrollbar-with-fixed-background.html +++ /dev/null @@ -1,68 +0,0 @@ -<!DOCTYPE html> -<html class=reftest-wait style="background: lightblue;"> -<title>When the root element has scrollbars, these should be excluded in snapshot</title> -<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> -<link rel="author" href="mailto:khushalsagar@chromium.org"> -<link rel="match" href="root-scrollbar-with-fixed-background-ref.html"> -<meta name="fuzzy" content="maxDifference=0-2; totalPixels=0-4500"> - -<script src="/common/rendering-utils.js"></script> -<script src="/common/reftest-wait.js"></script> - -<style> -#hide { - position: absolute; - top: 0px; - left: 0px; - width: 10px; - height: 10px; - background: red; - contain: paint; - view-transition-name: hide; -} -#first { - width: 10px; - background: linear-gradient(green, blue); - height: 1000px; -} -body { - margin: 0px; - padding: 0px; -} - -/* Set a no-op animation to screenshot the pseudo transition DOM. */ -html::view-transition-group(hide) { - animation-duration: 300s; - opacity: 0; -} -html::view-transition-new(*) { - animation: unset; - filter: invert(1); - height: 100%; -} -html::view-transition-old(*) { - animation: unset; - opacity: 0; - height: 100%; -} -</style> - -<div id=hide></div> -<div id=first></div> - -<script> -failIfNot(document.startViewTransition, "Missing document.startViewTransition"); - -async function runTest() { - await waitForAtLeastOneFrame(); - - document.documentElement.scrollTop = 500; - document.startViewTransition(() => { - requestAnimationFrame(() => requestAnimationFrame(takeScreenshot)); - }); -} - -onload = () => requestAnimationFrame(runTest); -</script> - -</html> diff --git a/tests/wpt/tests/css/css-viewport/computedStyle-zoom.html b/tests/wpt/tests/css/css-viewport/computedStyle-zoom.html index 82af111bbe7..bd53ce535ce 100644 --- a/tests/wpt/tests/css/css-viewport/computedStyle-zoom.html +++ b/tests/wpt/tests/css/css-viewport/computedStyle-zoom.html @@ -9,6 +9,7 @@ div { width: 64px; height: 64px; + font-size: 64px; line-height: 64px; text-indent: 64px; background-color: blue @@ -29,8 +30,15 @@ <div class="x2_zoom" id="parent_div"> <div class="x4_zoom" id="nested_zoom"></div> </div> - <div class="x2_zoom" id="testing_set_style" style="height: 1px; width: 1px; line-height: 1px; text-indent: 1px;"></div> + <div class="x2_zoom" id="testing_set_style"></div> <script> + const LENGTH_PROPS = [ + "width", + "height", + "line-height", + "text-indent", + "font-size", + ]; test(function() { assert_true(!!no_zoom, "no zoom target exists"); assert_true(!!with_zoom, "zoom target exists"); @@ -38,7 +46,7 @@ assert_true(!!parent_div, "parent div with zoom exists") }); function assert_length_props(style, expected) { - for (let prop of ["width", "height", "line-height", "text-indent"]) { + for (let prop of LENGTH_PROPS) { assert_equals(style.getPropertyValue(prop), expected, prop); } } @@ -64,9 +72,10 @@ }); test(function(){ testDivStyle = getComputedStyle(testing_set_style); + testing_set_style.setAttribute("style", LENGTH_PROPS.map(p => p + ": 1px").join(";")); assert_length_props(testDivStyle, "1px"); assert_equals(testDivStyle.getPropertyValue("zoom"), "2"); - testing_set_style.setAttribute("style", "width: 64px; height: 64px; line-height: 64px; text-indent: 64px;"); + testing_set_style.setAttribute("style", LENGTH_PROPS.map(p => p + ": 64px").join(";")); assert_length_props(testDivStyle, "64px"); assert_equals(testDivStyle.getPropertyValue("zoom"), "2"); }); diff --git a/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom-nested.html b/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom-nested.html index 22a491eb0b5..9dc99a0fa55 100644 --- a/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom-nested.html +++ b/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom-nested.html @@ -1,38 +1,28 @@ <!DOCTYPE html> <title>nested iframes with CSS zoom</title> <link rel="author" title="Yotam Hacohen" href="mailto:yotha@chromium.org"> +<link rel="author" title="Stefan Zager" href="mailto:szager@chromium.org"> <link rel="author" title="Google" href="http://www.google.com/"> <link href="reference/iframe-zoom-nested-ref.html" rel="match"> <link rel="help" href="https://drafts.csswg.org/css-viewport/"> -<head> - <style> - body { - overflow: hidden; - } - div { - margin: 0px; - padding: 0px; - overflow: visible; - } +<style> +body { + columns: 2; + --iframe-width: 256px; + --iframe-height: 128px; +} +iframe { + border: none; + width: var(--iframe-width); + height: var(--iframe-height); +} +.zoom { + zoom: 1.5; +} +</style> - iframe { - overflow: visible; - border: none; - } - </style> -</head> -<body> - <div id="no_zoom"> - <iframe src="resources/nested-iframe-no-zoom.html" scrolling="no"></iframe> - </div> - <div id="no_zoom2"> - <iframe src="resources/nested-iframe-with-zoom.html" scrolling="no"></iframe> - </div> - <div id="with_zoom" style="zoom: 2;"> - <iframe src="resources/nested-iframe-no-zoom.html" scrolling="no"></iframe> - </div> - <div id="another_with_zoom" style="zoom: 2;"> - <iframe src="resources/nested-iframe-with-zoom.html" scrolling="no"></iframe> - </div> -</body> +<iframe id="baseline" src="resources/nested-iframe.html" scrolling="no"></iframe> +<iframe id="zoom-child" src="resources/nested-iframe.html?zoom=2" scrolling="no"></iframe> +<iframe id="zoom-top" class="zoom" src="resources/nested-iframe.html" scrolling="no"></iframe> +<iframe id="zoom-top-child" class="zoom" src="resources/nested-iframe.html?zoom=2" scrolling="no"></iframe> diff --git a/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom.sub.html b/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom.sub.html index a27fb91619b..3ddfcd820d7 100644 --- a/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom.sub.html +++ b/tests/wpt/tests/css/css-viewport/zoom/iframe-zoom.sub.html @@ -1,41 +1,26 @@ <!DOCTYPE html> <title>iframe in an element with CSS zoom</title> <link rel="author" title="Yotam Hacohen" href="mailto:yotha@chromium.org"> +<link rel="author" title="Stefan Zager" href="mailto:szager@chromium.org"> <link rel="author" title="Google" href="http://www.google.com/"> <link href="reference/iframe-zoom-ref.html" rel="match"> <link rel="help" href="https://drafts.csswg.org/css-viewport/"> -<head> - <style> - body { - overflow: hidden; - } - - div { - margin: 0px; - padding: 0px; - } - - iframe { - height: 80px; - width: 80px; - border: none; - } - </style> -</head> - -<body> - - <div id="no_zoom"> - <iframe src="resources/iframe_content.html"></iframe> - </div> - - <div id="with_zoom" style="zoom: 3;"> - <iframe src="resources/iframe_content.html"></iframe> - </div> - - <div id="another_with_zoom" style="zoom: 3;"> - <iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/css-viewport/zoom/resources/iframe_content.html"></iframe> - </div> - -</body> +<style> +body { + --iframe-width: 128px; + --iframe-height: 64px; +} +iframe { + border: none; + width: var(--iframe-width); + height: var(--iframe-height); +} +.zoom { + zoom: 2; +} +</style> + +<iframe id="baseline" src="resources/leaf.html"></iframe> +<iframe id="zoom-same-origin" class="zoom" src="resources/leaf.html"></iframe> +<iframe id="zoom-cross-origin" class="zoom" src="http://{{hosts[alt][]}}:{{ports[http][0]}}/css/css-viewport/zoom/resources/leaf.html"></iframe> diff --git a/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html b/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html index b855278516c..fcdc16b1fb7 100644 --- a/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html +++ b/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-nested-ref.html @@ -1,35 +1,28 @@ <!DOCTYPE html> <title>ref for nested iframes with css zoom</title> <link rel="author" title="Yotam Hacohen" href="mailto:yotha@chromium.org"> +<link rel="author" title="Stefan Zager" href="mailto:szager@chromium.org"> <link rel="author" title="Google" href="http://www.google.com/"> <link rel="help" href="https://drafts.csswg.org/css-viewport/"> -<head> - <style> - iframe { - border: none; - margin: 0px; - padding: 0px; - } - </style> -</head> +<style> +body { + columns: 2; + --iframe-width: 256px; + --iframe-height: 128px; + --scale: 1; +} +iframe { + border: none; + width: calc(var(--iframe-width) * var(--scale)); + height: calc(var(--iframe-height) * var(--scale)); +} +.scale { + --scale: 1.5; +} +</style> -<body> - - <div id="no_zoom"> - <iframe style="height: 80px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px;"></div></body>'></iframe> - </div> - - <div id="with_zoom"> - <iframe style="height: 248px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px; zoom: 2;"></div></body>'></iframe> - </div> - - <div id="another_with_zoom"> - <iframe style="height: 248px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px; zoom: 2;"></div></body>'></iframe> - </div> - - <div id="another_with_zoom"> - <iframe style="height: 260px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px; zoom: 4;"></div></body>'></iframe> - </div> - -</body> +<iframe id="baseline-ref" src="../resources/nested-iframe.html" scrolling="no"></iframe> +<iframe id="zoom-child-ref" src="../resources/nested-iframe.html?subscale=2" scrolling="no"></iframe> +<iframe id="zoom-top-ref" class="scale" src="../resources/nested-iframe.html?topscale=1.5" scrolling="no"></iframe> +<iframe id="zoom-top-child-ref" class="scale" src="../resources/nested-iframe.html?topscale=1.5&subscale=2" scrolling="no"></iframe> diff --git a/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html b/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html index 43bc3e24cf8..f482d29ebf2 100644 --- a/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html +++ b/tests/wpt/tests/css/css-viewport/zoom/reference/iframe-zoom-ref.html @@ -1,31 +1,26 @@ <!DOCTYPE html> <title>ref for iframe in an element with css zoom</title> <link rel="author" title="Yotam Hacohen" href="mailto:yotha@chromium.org"> +<link rel="author" title="Stefan Zager" href="mailto:szager@chromium.org"> <link rel="author" title="Google" href="http://www.google.com/"> <link rel="help" href="https://drafts.csswg.org/css-viewport/"> -<head> - <style> - iframe { - border: none; - margin: 0px; - padding: 0px; - } - </style> -</head> +<style> +body { + --iframe-width: 128px; + --iframe-height: 64px; + --scale: 1; +} +iframe { + border: none; + width: calc(var(--iframe-width) * var(--scale)); + height: calc(var(--iframe-height) * var(--scale)); +} +.scale { + --scale: 2; +} +</style> -<body> - - <div id="no_zoom"> - <iframe style="height: 80px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px;"></div></body>'></iframe> - </div> - - <div id="with_zoom"> - <iframe style="height: 248px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px; zoom: 3;"></div></body>'></iframe> - </div> - - <div id="another_with_zoom"> - <iframe style="height: 240px;" srcdoc='<body style="margin: 0;"><div id="target" style="background-color: aqua; width: 64px; height: 64px; zoom: 3;"></div></body>'></iframe> - </div> - -</body> +<iframe id="baseline-ref" src="../resources/leaf.html"></iframe> +<iframe id="zoom-same-origin-ref" class="scale" src="../resources/leaf.html?scale=2"></iframe> +<iframe id="zoom-cross-origin-ref" class="scale" src="../resources/leaf.html?scale=2"></iframe> diff --git a/tests/wpt/tests/css/css-viewport/zoom/resources/leaf.html b/tests/wpt/tests/css/css-viewport/zoom/resources/leaf.html new file mode 100644 index 00000000000..21e75b62768 --- /dev/null +++ b/tests/wpt/tests/css/css-viewport/zoom/resources/leaf.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<style> +body { + background-color: aqua; + --target-width: 32px; + --target-height: 24px; + --scale: 1; + margin: calc(18px * var(--scale)); +} +#target { + width: calc(var(--target-width) * var(--scale)); + height: calc(var(--target-height) * var(--scale)); + background-color: hotpink; +} +</style> + +<div id="target"></div> + +<script> +let params = new URLSearchParams(location.search); +if (params.has("scale")) { + document.body.style.setProperty("--scale", parseFloat(params.get("scale"))); +} +</script> diff --git a/tests/wpt/tests/css/css-viewport/zoom/resources/nested-iframe.html b/tests/wpt/tests/css/css-viewport/zoom/resources/nested-iframe.html new file mode 100644 index 00000000000..f48d230e13d --- /dev/null +++ b/tests/wpt/tests/css/css-viewport/zoom/resources/nested-iframe.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> + +<style> +body { + --top-scale: 1; + --sub-scale: 1; + margin: calc(12px * var(--top-scale)); + background-color: orange; +} +iframe { + border: none; + width: calc(96px * var(--top-scale) * var(--sub-scale)); + height: calc(48px * var(--top-scale) * var(--sub-scale)); +} +</style> + +<iframe id="target" scrolling="no"></iframe> + +<script> +let target = document.getElementById("target"); +let params = new URLSearchParams(location.search); +let scale = 1; + +if (params.has("zoom")) { + target.style.zoom = parseFloat(params.get("zoom")); +} +if (params.has("topscale")) { + let topscale = parseFloat(params.get("topscale")); + document.body.style.setProperty("--top-scale", topscale); + scale *= topscale; +} +if (params.has("subscale")) { + let subscale = parseFloat(params.get("subscale")); + document.body.style.setProperty("--sub-scale", subscale); + scale *= subscale; +} + +let url = "leaf.html"; +if (scale != 1) { + url += `?scale=${scale}`; +} +target.src = url; +</script> diff --git a/tests/wpt/tests/css/css-writing-modes/mongolian-orientation-002.html b/tests/wpt/tests/css/css-writing-modes/mongolian-orientation-002.html index 66dde36ac09..82dabab9c2f 100644 --- a/tests/wpt/tests/css/css-writing-modes/mongolian-orientation-002.html +++ b/tests/wpt/tests/css/css-writing-modes/mongolian-orientation-002.html @@ -4,7 +4,7 @@ <link rel="author" title="Florian Rivoal" href="https://florian.rivoal.net/"> <link rel="help" href="https://www.w3.org/TR/css-writing-modes-3/#text-orientation"> <link rel="match" href="reference/mongolian-orientation-001-ref.html"> -<meta name="assert" content="In Mongolian, in horizontal text, glyphs are typeset in a 90° counter-clockwise rotation from their intrisic vertical orientation. text-orientation:sideways causes all text to be typeset sideways, as if in a horizontal layout, but rotated 90° clockwise. text-orientation:mixed causes typographic character units from vertical scripts are typeset with their intrinsic orientation. These two should therefore result in the same thing"> +<meta name="assert" content="In Mongolian, in horizontal text, glyphs are typeset in a 90° counter-clockwise rotation from their intrinsic vertical orientation. text-orientation:sideways causes all text to be typeset sideways, as if in a horizontal layout, but rotated 90° clockwise. text-orientation:mixed causes typographic character units from vertical scripts are typeset with their intrinsic orientation. These two should therefore result in the same thing"> <style> div { font-family: "Mongolian White"; /* Not required for the test to work, diff --git a/tests/wpt/tests/css/cssom-view/smooth-scroll-nonstop.html b/tests/wpt/tests/css/cssom-view/smooth-scroll-nonstop.html new file mode 100644 index 00000000000..7ba6e690f90 --- /dev/null +++ b/tests/wpt/tests/css/cssom-view/smooth-scroll-nonstop.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<html> +<head> +<title>Noop smooth scrolls don't interrupt ongoing smooth scrolls</title> +<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> +<link rel="help" href="https://drafts.csswg.org/cssom-view/#concept-smooth-scroll"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/css/css-scroll-snap/support/common.js"></script> +<script src="/dom/events/scrolling/scroll_support.js"></script> +</head> +<body> + <style> + .scroller { + height: 200px; + width: 200px; + border: solid 1px black; + background-color: teal; + overflow-y: scroll; + overflow-x: hidden; + position: relative; + } + .box { + height: 50px; + width: 50px; + background-color: purple; + margin-top: 400px; + } + .space { + position: absolute; + height: 200vh; + width: 200vw; + } + </style> + <div id="scroller" class="scroller"> + <div class="space"></div> + <div id="box" class="box"></div> + </div> + <script> + const scroller = document.getElementById("scroller"); + const box = document.getElementById("box"); + + async function test_smooth_scroll_nonstop(test, scroll_function, target_offset) { + await waitForScrollReset(test, scroller); + await waitForCompositorCommit(test, scroller); + + const scrollend_promise = waitForScrollEnd(scroller); + const MAX_NO_MOVE_DURATION_MS = 1000; + let resolve_stuck_promise = null; + const stuck_promise = new Promise((resolve) => { + resolve_stuck_promise = resolve; + }); + let last_observed_offset = scroller.scrollTop; + let last_update_time = performance.now; + function run_scroll_function() { + if (scroller.scrollTop != last_observed_offset) { + last_update_time = performance.now(); + last_observed_offset = scroller.scrollTop; + } else { + if (performance.now() - last_update_time > MAX_NO_MOVE_DURATION_MS) { + resolve_stuck_promise(); + } + } + scroll_function(); + } + + // Run the scroll function repeatedly. + const id = setInterval(run_scroll_function); + await Promise.any([scrollend_promise, stuck_promise]); + + clearInterval(id); + assert_equals(scroller.scrollTop, target_offset, + "scroller reached the target offset"); + } + + promise_test(async (t) => { + const target_offset = box.offsetTop; + const scroll_function = () => { + scroller.scrollTo({ top: target_offset, behavior: "smooth" }); + } + await test_smooth_scroll_nonstop(test, scroll_function, target_offset); + }, "noop scrollTo doesn't interrupt ongoing smooth scroll."); + + promise_test(async (t) => { + const target_offset = box.offsetTop; + const scroll_function = () => { + box.scrollIntoView({ block: "start", behavior: "smooth" }); + } + await test_smooth_scroll_nonstop(test, scroll_function, target_offset); + }, "noop scrollIntoView doesn't interrupt ongoing smooth scroll."); + </script> +</body> +</html> diff --git a/tests/wpt/tests/digital-credentials/digital-credentials.tentative.https.html b/tests/wpt/tests/digital-credentials/digital-credentials.tentative.https.html deleted file mode 100644 index 30e24e5450a..00000000000 --- a/tests/wpt/tests/digital-credentials/digital-credentials.tentative.https.html +++ /dev/null @@ -1,122 +0,0 @@ -<!DOCTYPE html> -<title>Digital Identity Credential tests.</title> -<link rel="help" href="https://wicg.github.io/digital-identities/"> -<script src="/common/get-host-info.sub.js"></script> -<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 { buildValidNavigatorIdentityRequest, requestIdentityWithActivation } from './support/digital-identity-helper.js'; - -// This regex removes the filename from the path so that we just get -// the directory. -const host = get_host_info(); -const basePath = window.location.pathname.replace(/\/[^\/]*$/, '/'); -const remoteBaseURL = host.HTTPS_REMOTE_ORIGIN + basePath; - -async function createIframeAndWaitForMessage(test, iframeUrl) { - const messageWatcher = new EventWatcher(test, window, "message"); - var iframe = document.createElement("iframe"); - iframe.src = iframeUrl; - document.body.appendChild(iframe); - const message = await messageWatcher.wait_for("message"); - return message.data; -} - -// Builds a valid navigator.identity.get() request where IdentityRequestProvider#request is an object. -function buildValidNavigatorIdentityRequestWithRequestObject() { - return { - digital: { - providers: [{ - protocol: "urn:openid.net:oid4vp", - request: { - // Based on https://github.com/openid/OpenID4VP/issues/125 - client_id: "client.example.org", - client_id_scheme: "web-origin", - nonce: "n-0S6_WzA2Mj", - presentation_definition: { - // Presentation Exchange request, omitted for brevity - } - }, - }], - }, - }; -} - -// Requires browser to have mode where OS-presented digital-identity-prompt is -// bypassed in favour of returning "fake_test_token" directly. -promise_test(async t=>{ - assert_false(navigator.userActivation.isActive); - let request = buildValidNavigatorIdentityRequest(); - await promise_rejects_dom(t, "NetworkError", navigator.identity.get(request)); -}, "navigator.identity.get() fails if the page doesn't have user activation"); - -promise_test(async t => { - let request = buildValidNavigatorIdentityRequest(); - let credential = await requestIdentityWithActivation(test_driver, request); - assert_equals("urn:openid.net:oid4vp", credential.protocol); - assert_equals("fake_test_token", credential.data); -}, "navigator.identity.get() API works in toplevel frame."); - -promise_test(async t => { - let request = buildValidNavigatorIdentityRequest(); - request.digital.providers = undefined; - await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); -}, "navigator.identity.get() API fails if DigitalCredentialRequestOptions::providers is not specified."); - -promise_test(async t => { - let request = buildValidNavigatorIdentityRequest(); - request.digital.providers = []; - await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); -}, "navigator.identity.get() API fails if there are no providers."); - -promise_test(async t => { - let request = buildValidNavigatorIdentityRequest(); - let providerCopy = structuredClone(request.digital.providers[0]); - request.digital.providers.push(providerCopy); - await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); -}, "navigator.identity.get() API fails if there is more than one provider."); - -promise_test(async t => { - const request = buildValidNavigatorIdentityRequestWithRequestObject(); - let credential = await requestIdentityWithActivation(test_driver, request); - assert_equals("urn:openid.net:oid4vp", credential.protocol); - assert_equals("fake_test_token", credential.data); -}, "navigator.identity.get() API succeeds when IdentityRequestProvider#request is an object instead of stringified JSON object"); - -promise_test(async t => { - const request = buildValidNavigatorIdentityRequestWithRequestObject(); - const largeList = []; - for (let i = 0; i < 1000000; ++i) { - largeList.push("Value " + i); - } - request.digital.providers[0].request.random = largeList; - await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); -}, "navigator.identity.get() API fails when IdentityRequestProvider#request object is too big"); - -promise_test(async t=> { - let abortController = new AbortController(); - let request = buildValidNavigatorIdentityRequest(); - request.signal = abortController.signal; - let requestPromise = requestIdentityWithActivation(test_driver, request); - abortController.abort(); - await promise_rejects_dom(t, "AbortError", requestPromise); -}, "navigator.identity.get() promise is rejected when the page aborts the request."); - -promise_test(async t=> { - const message = await createIframeAndWaitForMessage( - t, basePath + "support/digital-identity-iframe.html"); - assert_equals(message.result, "Pass"); - assert_equals(message.data, "fake_test_token"); -}, "navigator.identity.get() succeeds in same-origin iframe"); - -promise_test(async t=> { - const message = await createIframeAndWaitForMessage( - t, remoteBaseURL + "support/digital-identity-iframe.html"); - assert_equals(message.result, "Fail"); - assert_equals(message.errorType, "NotAllowedError"); -}, "navigator.identity.get() fails in cross-origin iframe"); -</script> diff --git a/tests/wpt/tests/digital-credentials/identity-get.tentative.https.html b/tests/wpt/tests/digital-credentials/identity-get.tentative.https.html new file mode 100644 index 00000000000..ba5212e433b --- /dev/null +++ b/tests/wpt/tests/digital-credentials/identity-get.tentative.https.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<title>Digital Identity Credential tests.</title> +<link rel="help" href="https://wicg.github.io/digital-identities/"> +<script src="/common/get-host-info.sub.js"></script> +<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 { buildValidNavigatorIdentityRequest, requestIdentityWithActivation } from './support/helper.js'; + +// This regex removes the filename from the path so that we just get +// the directory. +const host = get_host_info(); +const basePath = window.location.pathname.replace(/\/[^\/]*$/, '/'); +const remoteBaseURL = host.HTTPS_REMOTE_ORIGIN + basePath; + +async function createIframeAndWaitForMessage(test, iframeUrl) { + const messageWatcher = new EventWatcher(test, window, "message"); + var iframe = document.createElement("iframe"); + iframe.src = iframeUrl; + document.body.appendChild(iframe); + const message = await messageWatcher.wait_for("message"); + return message.data; +} + +// Builds a valid navigator.identity.get() request where IdentityRequestProvider#request is an object. +function buildValidNavigatorIdentityRequestWithRequestObject() { + return { + digital: { + providers: [{ + protocol: "urn:openid.net:oid4vp", + request: { + // Based on https://github.com/openid/OpenID4VP/issues/125 + client_id: "client.example.org", + client_id_scheme: "web-origin", + nonce: "n-0S6_WzA2Mj", + presentation_definition: { + // Presentation Exchange request, omitted for brevity + } + }, + }], + }, + }; +} + +// Requires browser to have mode where OS-presented digital-identity-prompt is +// bypassed in favour of returning "fake_test_token" directly. +promise_test(async t=>{ + assert_false(navigator.userActivation.isActive); + let request = buildValidNavigatorIdentityRequest(); + await promise_rejects_dom(t, "NetworkError", navigator.identity.get(request)); +}, "navigator.identity.get() fails if the page doesn't have user activation"); + +promise_test(async t => { + let request = buildValidNavigatorIdentityRequest(); + let credential = await requestIdentityWithActivation(test_driver, request); + assert_equals("urn:openid.net:oid4vp", credential.protocol); + assert_equals("fake_test_token", credential.data); +}, "navigator.identity.get() API works in toplevel frame."); + +promise_test(async t => { + let request = buildValidNavigatorIdentityRequest(); + request.digital.providers = undefined; + await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); +}, "navigator.identity.get() API fails if DigitalCredentialRequestOptions::providers is not specified."); + +promise_test(async t => { + let request = buildValidNavigatorIdentityRequest(); + request.digital.providers = []; + await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); +}, "navigator.identity.get() API fails if there are no providers."); + +promise_test(async t => { + let request = buildValidNavigatorIdentityRequest(); + let providerCopy = structuredClone(request.digital.providers[0]); + request.digital.providers.push(providerCopy); + await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); +}, "navigator.identity.get() API fails if there is more than one provider."); + +promise_test(async t => { + const request = buildValidNavigatorIdentityRequestWithRequestObject(); + let credential = await requestIdentityWithActivation(test_driver, request); + assert_equals("urn:openid.net:oid4vp", credential.protocol); + assert_equals("fake_test_token", credential.data); +}, "navigator.identity.get() API succeeds when IdentityRequestProvider#request is an object instead of stringified JSON object"); + +promise_test(async t => { + const request = buildValidNavigatorIdentityRequestWithRequestObject(); + const largeList = []; + for (let i = 0; i < 1000000; ++i) { + largeList.push("Value " + i); + } + request.digital.providers[0].request.random = largeList; + await promise_rejects_js(t, TypeError, requestIdentityWithActivation(test_driver, request)); +}, "navigator.identity.get() API fails when IdentityRequestProvider#request object is too big"); + +promise_test(async t=> { + let abortController = new AbortController(); + let request = buildValidNavigatorIdentityRequest(); + request.signal = abortController.signal; + let requestPromise = requestIdentityWithActivation(test_driver, request); + abortController.abort(); + await promise_rejects_dom(t, "AbortError", requestPromise); +}, "navigator.identity.get() promise is rejected when the page aborts the request."); + +promise_test(async t=> { + const message = await createIframeAndWaitForMessage( + t, basePath + "support/iframe.html"); + assert_equals(message.result, "Pass"); + assert_equals(message.data, "fake_test_token"); +}, "navigator.identity.get() succeeds in same-origin iframe"); + +promise_test(async t=> { + const message = await createIframeAndWaitForMessage( + t, remoteBaseURL + "support/iframe.html"); + assert_equals(message.result, "Fail"); + assert_equals(message.errorType, "NotAllowedError"); +}, "navigator.identity.get() fails in cross-origin iframe"); +</script> diff --git a/tests/wpt/tests/digital-credentials/support/digital-identity-iframe.html b/tests/wpt/tests/digital-credentials/support/digital-identity-iframe.html deleted file mode 100644 index 8b3a424d1e2..00000000000 --- a/tests/wpt/tests/digital-credentials/support/digital-identity-iframe.html +++ /dev/null @@ -1,27 +0,0 @@ -<!doctype html> -<script src="/resources/testdriver.js"></script> -<script src="/resources/testdriver-vendor.js"></script> -<script type="module"> -import { buildValidNavigatorIdentityRequest, requestIdentityWithActivation } from './digital-identity-helper.js'; - -// Loading digital-identity-iframe.html in the test will make a digital credential call on load, and -// trigger a postMessage upon completion. -// -// message { -// string result: "Pass" | "Fail" -// string data: credential.token -// string errorType: error.data -// } - -window.onload = async () => { - try { - let request = buildValidNavigatorIdentityRequest(); - let credential = await requestIdentityWithActivation(test_driver, request); - - window.top.postMessage({result: "Pass", data: credential.data}, '*'); - } catch (error) { - window.top.postMessage({result: "Fail", errorType: error.name}, '*'); - } -}; - -</script> diff --git a/tests/wpt/tests/digital-credentials/support/digital-identity-helper.js b/tests/wpt/tests/digital-credentials/support/helper.js index 8fff8274517..8fff8274517 100644 --- a/tests/wpt/tests/digital-credentials/support/digital-identity-helper.js +++ b/tests/wpt/tests/digital-credentials/support/helper.js diff --git a/tests/wpt/tests/digital-credentials/support/iframe.html b/tests/wpt/tests/digital-credentials/support/iframe.html new file mode 100644 index 00000000000..74733d82ec6 --- /dev/null +++ b/tests/wpt/tests/digital-credentials/support/iframe.html @@ -0,0 +1,27 @@ +<!doctype html> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script type="module"> +import { buildValidNavigatorIdentityRequest, requestIdentityWithActivation } from './helper.js'; + +// Loading digital-identity-iframe.html in the test will make a digital credential call on load, and +// trigger a postMessage upon completion. +// +// message { +// string result: "Pass" | "Fail" +// string data: credential.token +// string errorType: error.data +// } + +window.onload = async () => { + try { + let request = buildValidNavigatorIdentityRequest(); + let credential = await requestIdentityWithActivation(test_driver, request); + + window.top.postMessage({result: "Pass", data: credential.data}, '*'); + } catch (error) { + window.top.postMessage({result: "Fail", errorType: error.name}, '*'); + } +}; + +</script> diff --git a/tests/wpt/tests/dom/events/EventTarget-constructible.any.js b/tests/wpt/tests/dom/events/EventTarget-constructible.any.js index b0e7614e625..4125d23f0c9 100644 --- a/tests/wpt/tests/dom/events/EventTarget-constructible.any.js +++ b/tests/wpt/tests/dom/events/EventTarget-constructible.any.js @@ -24,6 +24,23 @@ test(() => { }, "A constructed EventTarget can be used as expected"); test(() => { + const target = new EventTarget(); + const event = new Event("foo"); + + function listener(e) { + assert_equals(e, event); + assert_equals(e.target, target); + assert_equals(e.currentTarget, target); + assert_array_equals(e.composedPath(), [target]); + } + target.addEventListener("foo", listener, { once: true }); + target.dispatchEvent(event); + assert_equals(event.target, target); + assert_equals(event.currentTarget, null); + assert_array_equals(event.composedPath(), []); +}, "A constructed EventTarget implements dispatch correctly"); + +test(() => { class NicerEventTarget extends EventTarget { on(...args) { this.addEventListener(...args); diff --git a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html b/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html index 58db5cd04ff..b2782dbfa9b 100644 --- a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html +++ b/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax-innerhtml.tentative.html @@ -13,9 +13,10 @@ </div> <script> -function assertIsComment(node,commentText) { +function assertIsComment(node, commentText) { assert_true(node instanceof Comment); - assert_equals(node.textContent,commentText); + // TODO(crbug.com/40271855): While developing alternative syntax, the comment might be empty or it might be "S"/"E". + assert_true(node.textContent === '' || node.textContent === commentText); } const declarativeOpenerNoParseparts = '<h1>'; @@ -32,7 +33,7 @@ Array.from(document.querySelectorAll('#context_elements>*')).forEach(contextEl = const root = contextEl.content ? contextEl.content.getPartRoot() : document.getPartRoot(); assert_equals(root.getParts().length,0,'Should start with no parts'); t.add_cleanup(() => { - contextEl.replaceChildren(); + (contextEl.content || contextEl).replaceChildren(); root.getParts().forEach(part => part.disconnect()); }); contextEl.innerHTML = content; @@ -40,13 +41,13 @@ Array.from(document.querySelectorAll('#context_elements>*')).forEach(contextEl = let expectedRootParts = [{type:'ChildNodePart',metadata:[]}]; assertEqualParts(root.getParts(),expectedRootParts,0,'declarative root missing parts'); const childPart1 = root.getParts()[0]; - assertIsComment(childPart1.previousSibling,''); - assertIsComment(childPart1.nextSibling,''); + assertIsComment(childPart1.previousSibling,'S'); + assertIsComment(childPart1.nextSibling,'E'); const expectedChild1Parts = [{type:'ChildNodePart',metadata:[]}]; assertEqualParts(childPart1.getParts(),expectedChild1Parts,0,'First level childpart should just have one child part'); const childPart2 = childPart1.getParts()[0]; - assertIsComment(childPart2.previousSibling,''); - assertIsComment(childPart2.nextSibling,''); + assertIsComment(childPart2.previousSibling,'S'); + assertIsComment(childPart2.nextSibling,'E'); const expectedChild2Parts = [{type:'NodePart',metadata:[]}]; assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part'); assert_true(childPart2.getParts()[0].node instanceof HTMLSpanElement); @@ -81,7 +82,7 @@ test((t) => { tmpl.remove(); root.getParts().forEach(part => part.disconnect()); }); - assert_equals(root.getParts().length,0,'Declarative AttributeParts should be automatic, and should not show up in getParts()'); + assertEqualParts(root.getParts(),[{type:'AttributePart',metadata:[]},{type:'AttributePart',metadata:[]}],0,'Declarative AttributePart should be present'); function checkBasics(checkContent,expectId,expectClass) { const innerDiv = checkContent.firstElementChild; assert_equals(innerDiv.localName,'div'); @@ -93,9 +94,7 @@ test((t) => { checkBasics(tmpl.content); const clone = root.clone(); const clonedDiv = checkBasics(clone.rootContainer); - const cloneWithValues = root.clone({attributeValues: ['foo','bar']}); - const clonedDiv2 = checkBasics(cloneWithValues.rootContainer,'foo','bar'); -}, `Basic AttributePart cloning with values`); +}, `Basic AttributePart parsing with multiple attributes`); </script> diff --git a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html b/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html index 001ac24a445..3a437038737 100644 --- a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html +++ b/tests/wpt/tests/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html @@ -123,10 +123,12 @@ const template = document.getElementById('declarative'); const childPart2 = childPart1.getParts()[0]; assertIsComment(childPart2.previousSibling,''); assertIsComment(childPart2.nextSibling,''); - const expectedChild2Parts = [{type:'NodePart',metadata:[]}]; - assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part (AttributePart is automatic)'); + const expectedChild2Parts = [{type:'NodePart',metadata:[]},{type:'AttributePart',metadata:[]}]; + assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have NodePart and AttributePart'); assert_true(childPart2.getParts()[0].node instanceof HTMLSpanElement); assert_equals(childPart2.getParts()[0].node.textContent,'Middle'); + assert_true(childPart2.getParts()[1].node instanceof HTMLSpanElement); + assert_equals(childPart2.getParts()[1].node.textContent,'Middle'); } else { assertEqualParts(root.getParts(),[],[]); } diff --git a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html b/tests/wpt/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html deleted file mode 100644 index f6460353949..00000000000 --- a/tests/wpt/tests/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html +++ /dev/null @@ -1,129 +0,0 @@ -<!DOCTYPE html> -<title>DOM Parts: Basic object structure, <?child-node-part?> declarative API</title> -<meta name="author" href="mailto:masonf@chromium.org"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="./resources/domparts-utils.js"></script> - -<div> - <!-- Note - the test will remove this chunk of DOM once the test completes --> - <div id=target2> - Declarative syntax - The *two* templates below should have IDENTICAL STRUCTURE - to this one. There are four cases to test: - 1. Main document parsing (this chunk) - 2. Template parsing (the template below with id=declarative) - 3. Template/fragment cloning (a clone of the template with id=declarative) - 4. Declarative Shadow DOM parsing (template with id=declarative_shadow_dom and shadowrootmode attribute) - <h1 id="name"> - <?child-node-part fullname?> - First - <!--?child-node-part middle?--> <?node-part middle-node?>Middle <?/child-node-part middle?> - Last - <!-- ?/child-node-part foobar? --> - </h1> - Email: <?node-part email-link?><a id="link"></a> - - Here are some invalid parts that should not get parsed: - <!--child-node-part test comment without leading ?--> - <child-node-part test PI without leading ?> - <!--?child-node-partfoobar?--> - <?child-node-partfoobar?> - </div> -</div> -<template id=declarative> - <div> - <div id=target3>Declarative syntax - <h1 id="name"> - <?child-node-part fullname?> - First - <!--?child-node-part middle?--> <?node-part middle-node?>Middle <?/child-node-part middle?> - Last - <!-- ?/child-node-part foobar? --> - </h1> - Email: <?node-part email-link?><a id="link"></a> - - Here are some invalid parts that should not get parsed: - <!--child-node-part test comment without leading ?--> - <child-node-part test PI without leading ?> - <!--?child-node-partfoobar?--> - <?child-node-partfoobar?> - </div> - </div> -</template> - -<!-- TODO: This test should look at declarative shadow DOM behavior. --> - -<script> { -const template = document.getElementById('declarative'); -['Main Document','Template','Clone','PartClone'].forEach(testCase => { - function assertIsComment(node,commentText) { - assert_true(node instanceof Comment); - assert_equals(node.textContent,commentText); - } - - test((t) => { - let doc,target,wrapper,cleanup; - let expectDOMParts = true; - switch (testCase) { - case 'Main Document': - doc = document; - target = doc.querySelector('#target2'); - cleanup = [target.parentElement]; - break; - case 'Template': - doc = template.content; - target = doc.querySelector('#target3'); - cleanup = []; - break; - case 'Clone': - doc = document; - wrapper = document.body.appendChild(document.createElement('div')); - wrapper.appendChild(template.content.cloneNode(true)); - target = wrapper.querySelector('#target3'); - // A "normal" tree clone should not keep DOM Parts: - expectDOMParts = false; - cleanup = [wrapper]; - break; - case 'PartClone': - doc = document; - wrapper = document.body.appendChild(document.createElement('div')); - assert_true(template.content.getPartRoot().getParts().length != 0); - wrapper.appendChild(template.content.getPartRoot().clone().rootContainer); - target = wrapper.querySelector('#target3'); - // Even a PartRoot clone should not add parts to the document, when that - // clone is appendChilded to the document. - expectDOMParts = false; - cleanup = [wrapper]; - break; - default: - assert_unreached('Invalid test case'); - } - assert_true(!!(doc && target && target.parentElement)); - const root = doc.getPartRoot(); - t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup - t.add_cleanup(() => root.getParts().forEach(part => part.disconnect())); - - assert_true(root instanceof DocumentPartRoot); - if (expectDOMParts) { - const expectedRootParts = [{type:'ChildNodePart',metadata:['fullname']},{type:'NodePart',metadata:['email-link']}]; - assertEqualParts(root.getParts(),expectedRootParts,0,'declarative root should have two parts'); - assert_equals(root.getParts()[1].node,target.querySelector('#link')); - const childPart1 = root.getParts()[0]; - assertIsComment(childPart1.previousSibling,'?child-node-part fullname?'); - assertIsComment(childPart1.nextSibling,' ?/child-node-part foobar? '); - const expectedChild1Parts = [{type:'ChildNodePart',metadata:['middle']}]; - assertEqualParts(childPart1.getParts(),expectedChild1Parts,0,'First level childpart should just have one child part'); - const childPart2 = childPart1.getParts()[0]; - assertIsComment(childPart2.previousSibling,'?child-node-part middle?'); - assertIsComment(childPart2.nextSibling,'?/child-node-part middle?'); - const expectedChild2Parts = [{type:'NodePart',metadata:['middle-node']}]; - assertEqualParts(childPart2.getParts(),expectedChild2Parts,0,'Second level childpart should have just the node part'); - assert_true(childPart2.getParts()[0].node instanceof Text); - assert_equals(childPart2.getParts()[0].node.textContent,'Middle '); - } else { - assertEqualParts(root.getParts(),[],[]); - } - }, `Basic declarative DOM Parts (${testCase})`); -}); - -}</script> diff --git a/tests/wpt/tests/dom/parts/basic-dom-part-objects.tentative.html b/tests/wpt/tests/dom/parts/basic-dom-part-objects.tentative.html index d7834fe69b0..f2199362c4d 100644 --- a/tests/wpt/tests/dom/parts/basic-dom-part-objects.tentative.html +++ b/tests/wpt/tests/dom/parts/basic-dom-part-objects.tentative.html @@ -59,24 +59,15 @@ function addCleanup(t, part) { assert_throws_js(TypeError,() => new NodePart(nodePart,target.children[0]),'Constructing a Part with a NodePart as the PartRoot should throw'); - const attributePart = addCleanup(t,new AttributePart(root,target,'attributename',/*automatic*/false,{metadata: ['attribute-non-auto']})); - assertEqualParts([attributePart],[{type:'AttributePart',metadata:['attribute-non-auto']}],0,'Basic AttributePart'); + const attributePart = addCleanup(t,new AttributePart(root,target,'attributename',{metadata: ['attribute']})); + assertEqualParts([attributePart],[{type:'AttributePart',metadata:['attribute']}],0,'Basic AttributePart'); assert_equals(attributePart.node,target); assert_equals(attributePart.root,root); assert_equals(attributePart.localName,'attributename'); - assert_equals(attributePart.automatic,false); - runningPartsExpectation.push({type:'AttributePart',metadata:['attribute-non-auto']}); + runningPartsExpectation.push({type:'AttributePart',metadata:['attribute']}); assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart],'getParts() for the root should now have this attributePart'); assert_equals(parts.length,0,'Return value of getParts() is not live'); - const attributePartAuto = addCleanup(t,new AttributePart(root,target,'attributename',/*automatic*/true,{metadata: ['attribute-auto']})); - assertEqualParts([attributePartAuto],[{type:'AttributePart',metadata:['attribute-auto']}],0,'Basic automatic AttributePart'); - assert_equals(attributePartAuto.node,target); - assert_equals(attributePartAuto.root,root); - assert_equals(attributePartAuto.localName,'attributename'); - assert_equals(attributePartAuto.automatic,true); - assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,attributePart],'automatic AttributePart should not get included in getParts()'); - const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['bar','baz']})); assertEqualParts([childNodePart],[{type:'ChildNodePart',metadata:['bar','baz']}],0,'Basic ChildNodePart'); assert_equals(childNodePart.root,root); @@ -126,8 +117,7 @@ function addCleanup(t, part) { test((t) => { const root = doc.getPartRoot(); const nodePart = addCleanup(t,new NodePart(root,target,{metadata:['node1']})); - const attributePart = addCleanup(t,new AttributePart(root,target,'attributeName',/*automatic*/false,{metadata: ['attribute']})); - const nonTrackedAttributePart = addCleanup(t,new AttributePart(root,target,'attributeName',/*automatic*/true,{metadata: ['attribute-auto']})); + const attributePart = addCleanup(t,new AttributePart(root,target,'attributeName',{metadata: ['attribute']})); const childNodePart = addCleanup(t,new ChildNodePart(root,target.children[0], target.children[2],{metadata:['child']})); const nodePart3 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 3']})); const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[1].firstChild,{metadata: ['node 2']})); @@ -158,7 +148,6 @@ function addCleanup(t, part) { assert_not_equals(newAttributePart.node,target,'Node references should not point to original nodes'); assert_equals(newAttributePart.node.id,target.id,'New parts should point to cloned nodes'); assert_equals(newAttributePart.localName,attributePart.localName,'New attribute parts should carry over localName'); - assert_equals(newAttributePart.automatic,attributePart.automatic,'New attribute parts should carry over automatic'); assert_not_equals(newChildNodePart.previousSibling,a,'Node references should not point to original nodes'); assert_equals(newChildNodePart.previousSibling.id,'a'); assert_not_equals(newChildNodePart.nextSibling,c,'Node references should not point to original nodes'); diff --git a/tests/wpt/tests/dom/parts/resources/domparts-utils.js b/tests/wpt/tests/dom/parts/resources/domparts-utils.js index 5deeec80a35..f8982de50f3 100644 --- a/tests/wpt/tests/dom/parts/resources/domparts-utils.js +++ b/tests/wpt/tests/dom/parts/resources/domparts-utils.js @@ -3,9 +3,11 @@ function assertEqualParts(parts,partDescriptions,expectedParts,description) { for(let i=0;i<parts.length;++i) { assert_true(parts[i] instanceof Part,`${description}: not a Part`); assert_true(parts[i] instanceof window[partDescriptions[i].type],`${description}: index ${i} expected ${partDescriptions[i].type}`); - assert_array_equals(parts[i].metadata,partDescriptions[i].metadata,`${description}: index ${i} wrong metadata`); + // TODO(crbug.com/40271855): While developing alternative syntax, we aren't comparing the metadata: + // assert_array_equals(parts[i].metadata,partDescriptions[i].metadata,`${description}: index ${i} wrong metadata`); if (expectedParts) { - assert_equals(parts[i],expectedParts[i],`${description}: index ${i} object equality`); + // TODO(crbug.com/40271855): While developing alternative syntax, we aren't comparing equality of the Part objects: + // assert_equals(parts[i],expectedParts[i],`${description}: index ${i} object equality`); assert_equals(parts[i].root.getPartNode(i),parts[i].node || parts[i].previousSibling,'getPartNode() should return the same node as getParts().node/previousSibling'); } } diff --git a/tests/wpt/tests/domxpath/evaluator-cross-realm.tentative.html b/tests/wpt/tests/domxpath/evaluator-cross-realm.tentative.html new file mode 100644 index 00000000000..5bc1128658c --- /dev/null +++ b/tests/wpt/tests/domxpath/evaluator-cross-realm.tentative.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Cross-realm XPath evaluator</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function toArray(result) { + var a = []; + while (true) { + var node = result.iterateNext(); + if (node === null) break; + a.push(node); + } + return a; +} + +var html_ns = "http://www.w3.org/1999/xhtml"; +var xml_doc = document.implementation.createDocument(html_ns, "html"); +var html_doc = document.implementation.createHTMLDocument(); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/dummy.xml"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_evaluator = new iframe.contentWindow.XPathEvaluator(); + assert_array_equals(toArray(iframe_evaluator.evaluate("//html", xml_doc)), []); +}, "evaluator from realm with XML associated document, context node in XML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/dummy.xml"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_evaluator = new iframe.contentWindow.XPathEvaluator(); + assert_array_equals(toArray(iframe_evaluator.evaluate("//html", html_doc)), [html_doc.documentElement]); +}, "evaluator from realm with XML associated document, context node in HTML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/blank.html"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_evaluator = new iframe.contentWindow.XPathEvaluator(); + assert_array_equals(toArray(iframe_evaluator.evaluate("//html", xml_doc)), []); +}, "evaluator from realm with HTML associated document, context node in XML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/blank.html"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_evaluator = new iframe.contentWindow.XPathEvaluator(); + assert_array_equals(toArray(iframe_evaluator.evaluate("//html", html_doc)), [html_doc.documentElement]); +}, "evaluator from realm with HTML associated document, context node in HTML document, no namespace resolver"); +</script> diff --git a/tests/wpt/tests/domxpath/evaluator-different-document.tentative.html b/tests/wpt/tests/domxpath/evaluator-different-document.tentative.html new file mode 100644 index 00000000000..8092eef4624 --- /dev/null +++ b/tests/wpt/tests/domxpath/evaluator-different-document.tentative.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Cross-document XPath evaluation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +function toArray(result) { + var a = []; + while (true) { + var node = result.iterateNext(); + if (node === null) break; + a.push(node); + } + return a; +} + +var html_ns = "http://www.w3.org/1999/xhtml"; +var xml_doc = document.implementation.createDocument(html_ns, "html"); +var html_doc = document.implementation.createHTMLDocument(); + +function ns_resolver(x) { + if (x === "html") { + return html_ns; + } else { + return null; + } +} + +test(function() { + assert_array_equals(toArray(xml_doc.evaluate("//html", xml_doc)), []); +}, "evaluate operation on XML document, context node in XML document, no namespace resolver"); + +test(function() { + assert_array_equals(toArray(html_doc.evaluate("//html", html_doc)), [html_doc.documentElement]); +}, "evaluate operation on HTML document, context node in HTML document, no namespace resolver"); + +test(function() { + assert_array_equals(toArray(xml_doc.evaluate("//html", html_doc)), [html_doc.documentElement]); +}, "evaluate operation on XML document, context node in HTML document, no namespace resolver"); + +test(function() { + assert_array_equals(toArray(html_doc.evaluate("//html", xml_doc)), []); +}, "evaluate operation on HTML document, context node in XML document, no namespace resolver"); + +test(function() { + assert_array_equals(toArray(xml_doc.evaluate("//html", xml_doc, ns_resolver)), []); +}, "evaluate operation on XML document, context node in XML document, with namespace resolver"); + +test(function() { + assert_array_equals(toArray(html_doc.evaluate("//html", html_doc, ns_resolver)), [html_doc.documentElement]); +}, "evaluate operation on HTML document, context node in HTML document, with namespace resolver"); + +test(function() { + assert_array_equals(toArray(xml_doc.evaluate("//html", html_doc, ns_resolver)), [html_doc.documentElement]); +}, "evaluate operation on XML document, context node in HTML document, with namespace resolver"); + +test(function() { + assert_array_equals(toArray(html_doc.evaluate("//html", xml_doc, ns_resolver)), []); +}, "evaluate operation on HTML document, context node in XML document, with namespace resolver"); +</script> diff --git a/tests/wpt/tests/domxpath/expression-cross-realm.tentative.html b/tests/wpt/tests/domxpath/expression-cross-realm.tentative.html new file mode 100644 index 00000000000..f75a949e730 --- /dev/null +++ b/tests/wpt/tests/domxpath/expression-cross-realm.tentative.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Cross-realm XPath evaluator</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function toArray(result) { + var a = []; + while (true) { + var node = result.iterateNext(); + if (node === null) break; + a.push(node); + } + return a; +} + +var html_ns = "http://www.w3.org/1999/xhtml"; +var xml_doc = document.implementation.createDocument(html_ns, "html"); +var html_doc = document.implementation.createHTMLDocument(); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/dummy.xml"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_expression = iframe.contentDocument.createExpression("//html"); + assert_array_equals(toArray(iframe_expression.evaluate(xml_doc)), []); +}, "expression from realm with XML associated document, context node in XML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/dummy.xml"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_expression = iframe.contentDocument.createExpression("//html"); + assert_array_equals(toArray(iframe_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from realm with XML associated document, context node in HTML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/blank.html"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_expression = iframe.contentDocument.createExpression("//html"); + assert_array_equals(toArray(iframe_expression.evaluate(xml_doc)), []); +}, "expression from realm with HTML associated document, context node in XML document, no namespace resolver"); + +promise_test(async (t) => { + const iframe = document.createElement("iframe"); + iframe.src = "/common/blank.html"; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", resolve, {once: true}); + }); + t.add_cleanup(() => iframe.remove()); + const iframe_expression = iframe.contentDocument.createExpression("//html"); + assert_array_equals(toArray(iframe_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from realm with HTML associated document, context node in HTML document, no namespace resolver"); +</script> diff --git a/tests/wpt/tests/domxpath/expression-different-document.tentative.html b/tests/wpt/tests/domxpath/expression-different-document.tentative.html new file mode 100644 index 00000000000..6e35d8b0267 --- /dev/null +++ b/tests/wpt/tests/domxpath/expression-different-document.tentative.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Cross-document XPath expressions</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +function toArray(result) { + var a = []; + while (true) { + var node = result.iterateNext(); + if (node === null) break; + a.push(node); + } + return a; +} + +var html_ns = "http://www.w3.org/1999/xhtml"; +var xml_doc = document.implementation.createDocument(html_ns, "html"); +var html_doc = document.implementation.createHTMLDocument(); + +function ns_resolver(x) { + if (x === "html") { + return html_ns; + } else { + return null; + } +} + +test(function() { + var xml_doc_expression = xml_doc.createExpression("//html"); + assert_array_equals(toArray(xml_doc_expression.evaluate(xml_doc)), []); +}, "expression from XML document, context node in XML document, no namespace resolver"); + +test(function() { + var html_doc_expression = html_doc.createExpression("//html"); + assert_array_equals(toArray(html_doc_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from HTML document, context node in HTML document, no namespace resolver"); + +test(function() { + var xml_doc_expression = xml_doc.createExpression("//html"); + assert_array_equals(toArray(xml_doc_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from XML document, context node in HTML document, no namespace resolver"); + +test(function() { + var html_doc_expression = html_doc.createExpression("//html"); + assert_array_equals(toArray(html_doc_expression.evaluate(xml_doc)), []); +}, "expression from HTML document, context node in XML document, no namespace resolver"); + +test(function() { + var xml_doc_expression = xml_doc.createExpression("//html", ns_resolver); + assert_array_equals(toArray(xml_doc_expression.evaluate(xml_doc)), []); +}, "expression from XML document, context node in XML document, with namespace resolver"); + +test(function() { + var html_doc_expression = html_doc.createExpression("//html", ns_resolver); + assert_array_equals(toArray(html_doc_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from HTML document, context node in HTML document, with namespace resolver"); + +test(function() { + var xml_doc_expression = xml_doc.createExpression("//html", ns_resolver); + assert_array_equals(toArray(xml_doc_expression.evaluate(html_doc)), [html_doc.documentElement]); +}, "expression from XML document, context node in HTML document, with namespace resolver"); + +test(function() { + var html_doc_expression = html_doc.createExpression("//html", ns_resolver); + assert_array_equals(toArray(html_doc_expression.evaluate(xml_doc)), []); +}, "expression from HTML document, context node in XML document, with namespace resolver"); +</script> diff --git a/tests/wpt/tests/editing/crashtests/insertparagraph-in-editable-dl-outside-body.html b/tests/wpt/tests/editing/crashtests/insertparagraph-in-editable-dl-outside-body.html new file mode 100644 index 00000000000..4c04d959075 --- /dev/null +++ b/tests/wpt/tests/editing/crashtests/insertparagraph-in-editable-dl-outside-body.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<script> +"use strict"; + +document.addEventListener("DOMContentLoaded", () => { + const dl = document.createElement("dl"); + const dd = document.createElement("dd"); + const p = document.createElement("p"); + dd.appendChild(p); + dl.appendChild(dd); + document.documentElement.appendChild(dl); + dl.contentEditable = true; + getSelection().collapse(p, 0); + document.execCommand("insertParagraph"); +}, {once: true}); +</script> +</head> +<body></body> +</html> diff --git a/tests/wpt/tests/fetch/api/basic/keepalive.any.js b/tests/wpt/tests/fetch/api/basic/keepalive.any.js index d6ec1f67920..55225e00aa0 100644 --- a/tests/wpt/tests/fetch/api/basic/keepalive.any.js +++ b/tests/wpt/tests/fetch/api/basic/keepalive.any.js @@ -18,7 +18,7 @@ const { * document event. */ function keepaliveSimpleRequestTest(method) { - for (const evt of ['load', 'pagehide', 'unload']) { + for (const evt of ['load', 'unload', 'pagehide']) { const desc = `[keepalive] simple ${method} request on '${evt}' [no payload]`; promise_test(async (test) => { @@ -30,7 +30,6 @@ function keepaliveSimpleRequestTest(method) { if (evt != 'load') { iframe.remove(); } - assert_equals(await getTokenFromMessage(), token1); assertStashedTokenAsync(desc, token1); }, `${desc}; setting up`); diff --git a/tests/wpt/tests/file-system-access/getDirectory.https.any.js b/tests/wpt/tests/file-system-access/getDirectory.https.any.js new file mode 100644 index 00000000000..bace6860b85 --- /dev/null +++ b/tests/wpt/tests/file-system-access/getDirectory.https.any.js @@ -0,0 +1,12 @@ +// META: global=window,worker +// META: script=resources/test-helpers.js + +promise_test(async t => { + const directory = await navigator.storage.getDirectory(); + return directory.getFileHandle("testFile", { create: true }); +}, "Call getFileHandle successfully"); + +promise_test(async t => { + const directory = await navigator.storage.getDirectory(); + return directory.getDirectoryHandle("testDirectory", { create: true }); +}, "Call getDirectoryHandle successfully"); diff --git a/tests/wpt/tests/geolocation-API/PositionOptions.https.html b/tests/wpt/tests/geolocation-API/PositionOptions.https.html deleted file mode 100644 index 6b36d66d735..00000000000 --- a/tests/wpt/tests/geolocation-API/PositionOptions.https.html +++ /dev/null @@ -1,93 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8" /> -<title>Geolocation Test: PositionOptions tests</title> -<link - rel="help" - href="http://www.w3.org/TR/geolocation-API/#position_options_interface" -/> -<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> - const resetPermission = () => { - return test_driver.set_permission({ name: "geolocation" }, "prompt"); - }; - const invalidValues = ["boom", 321, -Infinity, { foo: 5 }]; - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - for (const enableHighAccuracy of invalidValues) { - navigator.geolocation.getCurrentPosition(() => {}, null, { - enableHighAccuracy, - }); - } - }, "Call getCurrentPosition with wrong type for enableHighAccuracy. No exception expected."); - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - for (const enableHighAccuracy of invalidValues) { - const id = navigator.geolocation.watchPosition(() => {}, null, { - enableHighAccuracy, - }); - navigator.geolocation.clearWatch(id); - } - }, "Call watchPosition with wrong type for enableHighAccuracy. No exception expected."); - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - const error = await new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(reject, resolve, { - timeout: 0, - maxAge: 0, - }); - }); - assert_equals(error.code, GeolocationPositionError.TIMEOUT); - }, "Set timeout and maximumAge to 0, check that timeout error raised (getCurrentPosition)"); - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - let watchId; - const error = await new Promise((resolve, reject) => { - watchId = navigator.geolocation.watchPosition(reject, resolve, { - timeout: 0, - maxAge: 0, - }); - }); - assert_equals(error.code, GeolocationPositionError.TIMEOUT); - navigator.geolocation.clearWatch(watchId); - }, "Set timeout and maximumAge to 0, check that timeout error raised (watchPosition)"); - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - let watchId; - const error = await new Promise((resolve, reject) => { - watchId = navigator.geolocation.getCurrentPosition(reject, resolve, { - timeout: -100, - maxAge: -100, - }); - }); - assert_equals(error.code, GeolocationPositionError.TIMEOUT); - navigator.geolocation.clearWatch(watchId); - }, "Check that a negative timeout and maxAge values are clamped to 0 (getCurrentPosition)"); - - promise_test(async (t) => { - t.add_cleanup(resetPermission); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - let watchId; - const error = await new Promise((resolve, reject) => { - watchId = navigator.geolocation.watchPosition(reject, resolve, { - timeout: -100, - maxAge: -100, - }); - }); - assert_equals(error.code, GeolocationPositionError.TIMEOUT); - navigator.geolocation.clearWatch(watchId); - }, "Check that a negative timeout and maxAge values are clamped to 0 (watchPosition)"); -</script> diff --git a/tests/wpt/tests/geolocation-API/non-fully-active.https.html b/tests/wpt/tests/geolocation-API/non-fully-active.https.html deleted file mode 100644 index 7b381be7fff..00000000000 --- a/tests/wpt/tests/geolocation-API/non-fully-active.https.html +++ /dev/null @@ -1,105 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8" /> -<title>Geolocation Test: non-fully active document</title> -<link rel="help" href="https://github.com/w3c/geolocation-api/pull/97" /> -<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="support.js"></script> -<body></body> -<script> - promise_test(async (t) => { - t.add_cleanup(() => { - return test_driver.set_permission({ name: "geolocation" }, "prompt"); - }); - await test_driver.set_permission({ name: "geolocation" }, "granted"); - - // Create the iframe, wait for it to load... - const iframe = document.createElement("iframe"); - // ...wait for the iframe to load... - await new Promise((resolve) => { - iframe.src = "resources/iframe.html"; - iframe.allow = "geolocation"; - iframe.addEventListener("load", resolve, { once: true }); - document.body.appendChild(iframe); - }); - - // Steal geolocation. - const geo = iframe.contentWindow.navigator.geolocation; - - // No longer fully active. - iframe.remove(); - - // Try to watch a position while not fully active... - const watchError = await new Promise((resolve, reject) => { - const watchId = geo.watchPosition( - reject, // We don't want a position - resolve // We want an error! - ); - // Always return 0. - assert_equals( - watchId, - 0, - "watchId is 0 when document is not fully-active" - ); - // And again, to make sure it's not changing - const watchId2 = geo.watchPosition( - () => {}, - () => {} - ); - assert_equals( - watchId2, - 0, - "watchId remains 0 when document is not fully-active" - ); - }); - - assert_equals( - watchError.code, - GeolocationPositionError.POSITION_UNAVAILABLE, - "watchPosition() returns an error on non-fully-active document" - ); - - // Now try to get current position while not fully active... - const positionError = await new Promise((resolve, reject) => { - geo.getCurrentPosition( - reject, // We don't want a position - resolve // We want an error! - ); - }); - assert_equals( - positionError.code, - GeolocationPositionError.POSITION_UNAVAILABLE, - "getCurrentPosition() calls the errorCallback with POSITION_UNAVAILABLE" - ); - - // Re-attach, and go back to fully active. - document.body.appendChild(iframe); - iframe.contentWindow.opener = window; - await new Promise((resolve) => - iframe.addEventListener("load", resolve, { once: true }) - ); - - // And we are back to fully active. - let watchId; - let position = await new Promise((resolve, reject) => { - watchId = iframe.contentWindow.navigator.geolocation.watchPosition( - resolve, - reject - ); - }); - assert_true(Number.isInteger(watchId), "Expected some number for watchId"); - assert_true(Boolean(position), "Expected a position"); - - // Finally, let's get the position from the reattached document. - position = await new Promise((resolve, reject) => { - iframe.contentWindow.navigator.geolocation.getCurrentPosition( - resolve, - reject - ); - }); - assert_true(Boolean(position), "Expected a position"); - iframe.contentWindow.navigator.geolocation.clearWatch(watchId); - }, "non-fully active document behavior"); -</script> diff --git a/tests/wpt/tests/geolocation-API/permission.https.html b/tests/wpt/tests/geolocation-API/permission.https.html deleted file mode 100644 index 4062843349d..00000000000 --- a/tests/wpt/tests/geolocation-API/permission.https.html +++ /dev/null @@ -1,14 +0,0 @@ -<!doctype html> -<meta charset=utf-8> -<title>Test geolocation is a powerful feature via Permissions API</title> -<script src=/resources/testharness.js></script> -<script src=/resources/testharnessreport.js></script> - -<script> -promise_test(async (test) => { - const status = await navigator.permissions.query({ name: "geolocation" }); - assert_true(status instanceof PermissionStatus); - assert_equals(status.name, "geolocation", `permission's name must be "geolocation"`); - assert_equals(status.state, "prompt", `permission's state must be "prompt" by default`); -}, `Query "geolocation" powerful feature`); -</script> diff --git a/tests/wpt/tests/geolocation-API/META.yml b/tests/wpt/tests/geolocation/META.yml index e3d6c4dff8e..e3d6c4dff8e 100644 --- a/tests/wpt/tests/geolocation-API/META.yml +++ b/tests/wpt/tests/geolocation/META.yml diff --git a/tests/wpt/tests/geolocation/PositionOptions.https.html b/tests/wpt/tests/geolocation/PositionOptions.https.html new file mode 100644 index 00000000000..e54e8679aa9 --- /dev/null +++ b/tests/wpt/tests/geolocation/PositionOptions.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Geolocation Test: PositionOptions tests</title> +<link + rel="help" + href="http://www.w3.org/TR/geolocation-API/#position_options_interface" +/> +<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> + promise_setup(async ()=>{ + await test_driver.set_permission({ name: "geolocation" }, "granted"); + }); + + const invalidValues = ["boom", 321, -Infinity, { foo: 5 }]; + + promise_test(async (t) => { + for (const enableHighAccuracy of invalidValues) { + navigator.geolocation.getCurrentPosition(() => {}, null, { + enableHighAccuracy, + }); + } + }, "Call getCurrentPosition with wrong type for enableHighAccuracy. No exception expected."); + + promise_test(async (t) => { + for (const enableHighAccuracy of invalidValues) { + const id = navigator.geolocation.watchPosition(() => {}, null, { + enableHighAccuracy, + }); + navigator.geolocation.clearWatch(id); + } + }, "Call watchPosition with wrong type for enableHighAccuracy. No exception expected."); + + promise_test(async (t) => { + const error = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(reject, resolve, { + timeout: 0, + maxAge: 0, + }); + }); + assert_equals(error.code, GeolocationPositionError.TIMEOUT); + }, "Set timeout and maximumAge to 0, check that timeout error raised (getCurrentPosition)"); + + promise_test(async (t) => { + let watchId; + const error = await new Promise((resolve, reject) => { + watchId = navigator.geolocation.watchPosition(reject, resolve, { + timeout: 0, + maxAge: 0, + }); + }); + assert_equals(error.code, GeolocationPositionError.TIMEOUT); + navigator.geolocation.clearWatch(watchId); + }, "Set timeout and maximumAge to 0, check that timeout error raised (watchPosition)"); + + promise_test(async (t) => { + let watchId; + const error = await new Promise((resolve, reject) => { + watchId = navigator.geolocation.getCurrentPosition(reject, resolve, { + timeout: -100, + maxAge: -100, + }); + }); + assert_equals(error.code, GeolocationPositionError.TIMEOUT); + navigator.geolocation.clearWatch(watchId); + }, "Check that a negative timeout and maxAge values are clamped to 0 (getCurrentPosition)"); + + promise_test(async (t) => { + let watchId; + const error = await new Promise((resolve, reject) => { + watchId = navigator.geolocation.watchPosition(reject, resolve, { + timeout: -100, + maxAge: -100, + }); + }); + assert_equals(error.code, GeolocationPositionError.TIMEOUT); + navigator.geolocation.clearWatch(watchId); + }, "Check that a negative timeout and maxAge values are clamped to 0 (watchPosition)"); +</script> diff --git a/tests/wpt/tests/geolocation-API/clearWatch_TypeError.https.html b/tests/wpt/tests/geolocation/clearWatch_TypeError.https.html index 37ebeca885f..37ebeca885f 100644 --- a/tests/wpt/tests/geolocation-API/clearWatch_TypeError.https.html +++ b/tests/wpt/tests/geolocation/clearWatch_TypeError.https.html diff --git a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation/disabled-by-permissions-policy.https.sub.html index ec34a4998da..ec34a4998da 100644 --- a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation/disabled-by-permissions-policy.https.sub.html diff --git a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html.headers b/tests/wpt/tests/geolocation/disabled-by-permissions-policy.https.sub.html.headers index 26bfbc24964..26bfbc24964 100644 --- a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html.headers +++ b/tests/wpt/tests/geolocation/disabled-by-permissions-policy.https.sub.html.headers diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html b/tests/wpt/tests/geolocation/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html index d25afa52bb7..d25afa52bb7 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html +++ b/tests/wpt/tests/geolocation/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html b/tests/wpt/tests/geolocation/enabled-by-permission-policy-attribute.https.sub.html index 50b8475b810..50b8475b810 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute.https.sub.html +++ b/tests/wpt/tests/geolocation/enabled-by-permission-policy-attribute.https.sub.html diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation/enabled-by-permissions-policy.https.sub.html index 332e4cea16b..332e4cea16b 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation/enabled-by-permissions-policy.https.sub.html diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html.headers b/tests/wpt/tests/geolocation/enabled-by-permissions-policy.https.sub.html.headers index 774f4819e9b..774f4819e9b 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html.headers +++ b/tests/wpt/tests/geolocation/enabled-by-permissions-policy.https.sub.html.headers diff --git a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation/enabled-on-self-origin-by-permissions-policy.https.sub.html index 5940888b356..5940888b356 100644 --- a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation/enabled-on-self-origin-by-permissions-policy.https.sub.html diff --git a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html.headers b/tests/wpt/tests/geolocation/enabled-on-self-origin-by-permissions-policy.https.sub.html.headers index 9fa0a4ac9a8..9fa0a4ac9a8 100644 --- a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html.headers +++ b/tests/wpt/tests/geolocation/enabled-on-self-origin-by-permissions-policy.https.sub.html.headers diff --git a/tests/wpt/tests/geolocation-API/getCurrentPosition_TypeError.https.html b/tests/wpt/tests/geolocation/getCurrentPosition_TypeError.https.html index 9da8ac1d292..9da8ac1d292 100644 --- a/tests/wpt/tests/geolocation-API/getCurrentPosition_TypeError.https.html +++ b/tests/wpt/tests/geolocation/getCurrentPosition_TypeError.https.html diff --git a/tests/wpt/tests/geolocation-API/getCurrentPosition_permission_allow.https.html b/tests/wpt/tests/geolocation/getCurrentPosition_permission_allow.https.html index c7fa970e086..c7fa970e086 100644 --- a/tests/wpt/tests/geolocation-API/getCurrentPosition_permission_allow.https.html +++ b/tests/wpt/tests/geolocation/getCurrentPosition_permission_allow.https.html diff --git a/tests/wpt/tests/geolocation-API/getCurrentPosition_permission_deny.https.html b/tests/wpt/tests/geolocation/getCurrentPosition_permission_deny.https.html index 54bba9aeabf..54bba9aeabf 100644 --- a/tests/wpt/tests/geolocation-API/getCurrentPosition_permission_deny.https.html +++ b/tests/wpt/tests/geolocation/getCurrentPosition_permission_deny.https.html diff --git a/tests/wpt/tests/geolocation-API/idlharness.https.window.js b/tests/wpt/tests/geolocation/idlharness.https.window.js index eadd6b7a4fb..eadd6b7a4fb 100644 --- a/tests/wpt/tests/geolocation-API/idlharness.https.window.js +++ b/tests/wpt/tests/geolocation/idlharness.https.window.js diff --git a/tests/wpt/tests/geolocation/non-fully-active.https.html b/tests/wpt/tests/geolocation/non-fully-active.https.html new file mode 100644 index 00000000000..20fcdae12a0 --- /dev/null +++ b/tests/wpt/tests/geolocation/non-fully-active.https.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Geolocation Test: non-fully active document</title> +<link rel="help" href="https://github.com/w3c/geolocation-api/pull/97" /> +<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="support.js"></script> +<body></body> +<script> + promise_setup(async () => { + await test_driver.set_permission({ name: "geolocation" }, "granted"); + }); + + promise_test(async (t) => { + // Create the iframe, wait for it to load... + const iframe = document.createElement("iframe"); + await new Promise((resolve) => { + iframe.src = "resources/iframe.html"; + iframe.allow = "geolocation"; + iframe.addEventListener("load", resolve, { once: true }); + document.body.appendChild(iframe); + }); + + // Steal geolocation. + const geo = iframe.contentWindow.navigator.geolocation; + + // No longer fully active. + iframe.remove(); + + // Try to watch a position while not fully active... + const watchError = await new Promise((resolve, reject) => { + const watchId = geo.watchPosition( + reject, // We don't want a position + resolve // We want an error! + ); + // Always return 0. + assert_equals( + watchId, + 0, + "watchId is 0 when document is not fully-active" + ); + // And again, to make sure it's not changing + const watchId2 = geo.watchPosition( + () => {}, + () => {} + ); + assert_equals( + watchId2, + 0, + "watchId remains 0 when document is not fully-active" + ); + }); + + assert_equals( + watchError.code, + GeolocationPositionError.POSITION_UNAVAILABLE, + "watchPosition() returns an error on non-fully-active document" + ); + + // Now try to get current position while not fully active... + const positionError = await new Promise((resolve, reject) => { + geo.getCurrentPosition( + reject, // We don't want a position + resolve // We want an error! + ); + }); + assert_equals( + positionError.code, + GeolocationPositionError.POSITION_UNAVAILABLE, + "getCurrentPosition() calls the errorCallback with POSITION_UNAVAILABLE" + ); + + // Re-attach, and go back to fully active. + document.body.appendChild(iframe); + iframe.contentWindow.opener = window; + await new Promise((resolve) => + iframe.addEventListener("load", resolve, { once: true }) + ); + + // And we are back to fully active. + let watchId; + let position = await new Promise((resolve, reject) => { + watchId = iframe.contentWindow.navigator.geolocation.watchPosition( + resolve, + reject + ); + }); + assert_true(Number.isInteger(watchId), "Expected some number for watchId"); + assert_true(Boolean(position), "Expected a position"); + + // Finally, let's get the position from the reattached document. + position = await new Promise((resolve, reject) => { + iframe.contentWindow.navigator.geolocation.getCurrentPosition( + resolve, + reject + ); + }); + assert_true(Boolean(position), "Expected a position"); + iframe.contentWindow.navigator.geolocation.clearWatch(watchId); + }, "non-fully active document behavior"); +</script> diff --git a/tests/wpt/tests/geolocation-API/non-secure-contexts.http.html b/tests/wpt/tests/geolocation/non-secure-contexts.http.html index d491086cf8a..d491086cf8a 100644 --- a/tests/wpt/tests/geolocation-API/non-secure-contexts.http.html +++ b/tests/wpt/tests/geolocation/non-secure-contexts.http.html diff --git a/tests/wpt/tests/geolocation/permission.https.html b/tests/wpt/tests/geolocation/permission.https.html new file mode 100644 index 00000000000..aa46caf32c3 --- /dev/null +++ b/tests/wpt/tests/geolocation/permission.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Test geolocation is a powerful feature via Permissions API</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<script> + promise_test(async (test) => { + const status = await navigator.permissions.query({ name: "geolocation" }); + assert_true(status instanceof PermissionStatus); + assert_equals( + status.name, + "geolocation", + `permission's name must be "geolocation"` + ); + const states = ["prompt", "granted", "denied"]; + assert_true( + states.includes(status.state), + `permission's state must be one of ${states.join(", ")}` + ); + }, `Query "geolocation" powerful feature`); +</script> diff --git a/tests/wpt/tests/geolocation-API/resources/iframe.html b/tests/wpt/tests/geolocation/resources/iframe.html index 25cf3d92f39..25cf3d92f39 100644 --- a/tests/wpt/tests/geolocation-API/resources/iframe.html +++ b/tests/wpt/tests/geolocation/resources/iframe.html diff --git a/tests/wpt/tests/geolocation-API/support.js b/tests/wpt/tests/geolocation/support.js index 0f0e7f65b2c..0f0e7f65b2c 100644 --- a/tests/wpt/tests/geolocation-API/support.js +++ b/tests/wpt/tests/geolocation/support.js diff --git a/tests/wpt/tests/geolocation-API/watchPosition_TypeError.https.html b/tests/wpt/tests/geolocation/watchPosition_TypeError.https.html index 4ae7a890371..4ae7a890371 100644 --- a/tests/wpt/tests/geolocation-API/watchPosition_TypeError.https.html +++ b/tests/wpt/tests/geolocation/watchPosition_TypeError.https.html diff --git a/tests/wpt/tests/geolocation-API/watchPosition_permission_deny.https.html b/tests/wpt/tests/geolocation/watchPosition_permission_deny.https.html index d47f2b5594a..d47f2b5594a 100644 --- a/tests/wpt/tests/geolocation-API/watchPosition_permission_deny.https.html +++ b/tests/wpt/tests/geolocation/watchPosition_permission_deny.https.html diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix-expected.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix-expected.html new file mode 100644 index 00000000000..0d61a3e6091 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix</title> +<h1>2d.layer.non-invertible-matrix</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.html new file mode 100644 index 00000000000..0cd13b7c224 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix</title> +<h1>2d.layer.non-invertible-matrix</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); +</script> diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow-expected.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow-expected.html new file mode 100644 index 00000000000..0cb7e929272 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix.shadow</title> +<h1>2d.layer.non-invertible-matrix.shadow</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow.html new file mode 100644 index 00000000000..864935db63f --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.shadow.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix.shadow-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.shadow</title> +<h1>2d.layer.non-invertible-matrix.shadow</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); +</script> diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html new file mode 100644 index 00000000000..10bfc7a19b3 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix.with-render-states-and-filter</title> +<h1>2d.layer.non-invertible-matrix.with-render-states-and-filter</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html new file mode 100644 index 00000000000..c81732c1bd3 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.with-render-states-and-filter</title> +<h1>2d.layer.non-invertible-matrix.with-render-states-and-filter</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.globalAlpha = 0.8; + ctx.globalCompositeOperation = 'multiply'; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer({filter: + {name: 'dropShadow', dx: 10, dy: 0, stdDeviation: 0, + floodColor: 'rgba(128, 128, 255, 0.7)'}}); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix-expected.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix-expected.html new file mode 100644 index 00000000000..0d61a3e6091 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix</title> +<h1>2d.layer.non-invertible-matrix</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html new file mode 100644 index 00000000000..426268baea8 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix</title> +<h1>2d.layer.non-invertible-matrix</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const outputCanvas = document.getElementById("canvas"); + outputCanvas.getContext('2d').drawImage(canvas, 0, 0); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow-expected.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow-expected.html new file mode 100644 index 00000000000..0cb7e929272 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix.shadow</title> +<h1>2d.layer.non-invertible-matrix.shadow</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html new file mode 100644 index 00000000000..45c20b527fb --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix.shadow-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.shadow</title> +<h1>2d.layer.non-invertible-matrix.shadow</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const outputCanvas = document.getElementById("canvas"); + outputCanvas.getContext('2d').drawImage(canvas, 0, 0); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html new file mode 100644 index 00000000000..573168f7c42 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.shadow.w.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<html class="reftest-wait"> +<link rel="match" href="2d.layer.non-invertible-matrix.shadow-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.shadow</title> +<h1>2d.layer.non-invertible-matrix.shadow</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script id='myWorker' type='text/worker'> + self.onmessage = function(e) { + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const bitmap = canvas.transferToImageBitmap(); + self.postMessage(bitmap, bitmap); + }; +</script> +<script> + const blob = new Blob([document.getElementById('myWorker').textContent]); + const worker = new Worker(URL.createObjectURL(blob)); + worker.addEventListener('message', msg => { + const outputCtx = document.getElementById("canvas").getContext('2d'); + outputCtx.drawImage(msg.data, 0, 0); + document.documentElement.classList.remove("reftest-wait"); + }); + worker.postMessage(null); +</script> +</html> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html new file mode 100644 index 00000000000..19aafd6d1ab --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.w.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<html class="reftest-wait"> +<link rel="match" href="2d.layer.non-invertible-matrix-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix</title> +<h1>2d.layer.non-invertible-matrix</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script id='myWorker' type='text/worker'> + self.onmessage = function(e) { + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const bitmap = canvas.transferToImageBitmap(); + self.postMessage(bitmap, bitmap); + }; +</script> +<script> + const blob = new Blob([document.getElementById('myWorker').textContent]); + const worker = new Worker(URL.createObjectURL(blob)); + worker.addEventListener('message', msg => { + const outputCtx = document.getElementById("canvas").getContext('2d'); + outputCtx.drawImage(msg.data, 0, 0); + document.documentElement.classList.remove("reftest-wait"); + }); + worker.postMessage(null); +</script> +</html> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html new file mode 100644 index 00000000000..10bfc7a19b3 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>Canvas test: 2d.layer.non-invertible-matrix.with-render-states-and-filter</title> +<h1>2d.layer.non-invertible-matrix.with-render-states-and-filter</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html new file mode 100644 index 00000000000..ec69a96b702 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<link rel="match" href="2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.with-render-states-and-filter</title> +<h1>2d.layer.non-invertible-matrix.with-render-states-and-filter</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script> + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.globalAlpha = 0.8; + ctx.globalCompositeOperation = 'multiply'; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer({filter: + {name: 'dropShadow', dx: 10, dy: 0, stdDeviation: 0, + floodColor: 'rgba(128, 128, 255, 0.7)'}}); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const outputCanvas = document.getElementById("canvas"); + outputCanvas.getContext('2d').drawImage(canvas, 0, 0); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html new file mode 100644 index 00000000000..1fe9d99d6d7 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/layers/2d.layer.non-invertible-matrix.with-render-states-and-filter.w.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<html class="reftest-wait"> +<link rel="match" href="2d.layer.non-invertible-matrix.with-render-states-and-filter-expected.html"> +<title>Canvas test: 2d.layer.non-invertible-matrix.with-render-states-and-filter</title> +<h1>2d.layer.non-invertible-matrix.with-render-states-and-filter</h1> +<p class="desc">Test drawing layers when the transform is not invertible.</p> +<canvas id="canvas" width="200" height="200"> + <p class="fallback">FAIL (fallback content)</p> +</canvas> +<script id='myWorker' type='text/worker'> + self.onmessage = function(e) { + const canvas = new OffscreenCanvas(200, 200); + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.globalAlpha = 0.8; + ctx.globalCompositeOperation = 'multiply'; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer({filter: + {name: 'dropShadow', dx: 10, dy: 0, stdDeviation: 0, + floodColor: 'rgba(128, 128, 255, 0.7)'}}); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + + const bitmap = canvas.transferToImageBitmap(); + self.postMessage(bitmap, bitmap); + }; +</script> +<script> + const blob = new Blob([document.getElementById('myWorker').textContent]); + const worker = new Worker(URL.createObjectURL(blob)); + worker.addEventListener('message', msg => { + const outputCtx = document.getElementById("canvas").getContext('2d'); + outputCtx.drawImage(msg.data, 0, 0); + document.documentElement.classList.remove("reftest-wait"); + }); + worker.postMessage(null); +</script> +</html> diff --git a/tests/wpt/tests/html/canvas/tools/gentestutilsunion.py b/tests/wpt/tests/html/canvas/tools/gentestutilsunion.py index 28802407995..ab6f4e1cd08 100644 --- a/tests/wpt/tests/html/canvas/tools/gentestutilsunion.py +++ b/tests/wpt/tests/html/canvas/tools/gentestutilsunion.py @@ -389,10 +389,12 @@ class _Variant(): return self def _render_param(self, jinja_env: jinja2.Environment, - param_name: str) -> str: - """Get the specified variant parameter and render it with Jinja.""" - value = self.params[param_name] - return jinja_env.from_string(value).render(self.params) + param_name: str) -> None: + """Render the specified parameter in-place in the `params` dict.""" + value = self.params.get(param_name) + if value and isinstance(value, str): + self._params[param_name] = ( + jinja_env.from_string(value).render(self.params)) def _get_file_name(self) -> str: @@ -431,8 +433,8 @@ class _Variant(): variant_id: int) -> None: """Finalize this variant by adding computed param fields.""" self._params['id'] = variant_id - self._params['name'] = self._render_param(jinja_env, 'name') - self._params['desc'] = self._render_param(jinja_env, 'desc') + for param_name in ('name', 'desc', 'attributes'): + self._render_param(jinja_env, param_name) self._params['file_name'] = self._get_file_name() self._params['canvas_types'] = self._get_canvas_types() self._params['template_type'] = self._get_template_type() diff --git a/tests/wpt/tests/html/canvas/tools/yaml-new/layers.yaml b/tests/wpt/tests/html/canvas/tools/yaml-new/layers.yaml index a915f391ec2..59030a69914 100644 --- a/tests/wpt/tests/html/canvas/tools/yaml-new/layers.yaml +++ b/tests/wpt/tests/html/canvas/tools/yaml-new/layers.yaml @@ -217,6 +217,111 @@ color: luminosity: +- name: 2d.layer.non-invertible-matrix + desc: Test drawing layers when the transform is not invertible. + size: [200, 200] + code: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + reference: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + +- name: 2d.layer.non-invertible-matrix.shadow + desc: Test drawing layers when the transform is not invertible. + size: [200, 200] + code: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer(); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + reference: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + +- name: 2d.layer.non-invertible-matrix.with-render-states-and-filter + desc: Test drawing layers when the transform is not invertible. + size: [200, 200] + code: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + + // Anything below will be non-rasterizable. + ctx.scale(1, 0); + + ctx.globalAlpha = 0.8; + ctx.globalCompositeOperation = 'multiply'; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 20; + ctx.shadowColor = 'rgba(255, 165, 0, 0.6)'; + + // Open the layer with a non-invertible matrix. The whole layer will be + // non-rasterizable. + ctx.beginLayer({filter: + {name: 'dropShadow', dx: 10, dy: 0, stdDeviation: 0, + floodColor: 'rgba(128, 128, 255, 0.7)'}}); + + // Because the transform is global, the matrix is still non-invertible. + ctx.fillStyle = 'rgba(225, 0, 0, 1)'; + ctx.fillRect(40, 70, 50, 50); + + // Set a valid matrix in the middle of the layer. This makes the global + // matrix invertible again, but because because the layer was opened with a + // non-invertible matrix, the whole layer remains non-rasterizable. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.fillStyle = 'green'; + ctx.fillRect(70, 40, 50, 50); + + ctx.endLayer(); + reference: | + ctx.fillStyle = 'blue'; + ctx.fillRect(30, 30, 50, 50); + - name: 2d.layer.global-filter desc: Tests that layers ignore the global context filter. size: [150, 100] diff --git a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html new file mode 100644 index 00000000000..14650f3f320 --- /dev/null +++ b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/option-computed-style.tentative.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel=help href="https://github.com/openui/open-ui/issues/863#issuecomment-1769004174"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<select style="appearance:base-select"> + <option>option</option> +</select> + +<script> +test(() => { + assert_equals( + getComputedStyle(document.querySelector('option'), '::before').content, + `"\u2713" / ""`); +}, 'appearance:base-select options should have a checkmark with empty alt text.'); +</script> diff --git a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css index 7f4c8b6c448..0052b3863e2 100644 --- a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css +++ b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/resources/stylable-select-styles.css @@ -38,6 +38,13 @@ align-content: center; } +.stylable-select-option::before { + content: '\2713' / ''; +} +.stylable-select-option:not(.selected)::before { + visibility: hidden; +} + .stylable-select-button { color: FieldText; background-color: Field; diff --git a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-ref.html b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-ref.html index e99ca4d57a8..a27e662b1c4 100644 --- a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-ref.html +++ b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-child-button-and-datalist-ref.html @@ -10,7 +10,7 @@ <div id=container class=stylable-select-container> <button popovertarget=popover id=button>button</button> <div id=popover popover=auto anchor=container class=stylable-select-datalist> - <div tabindex=0 class=stylable-select-option> + <div tabindex=0 class="stylable-select-option selected"> <span class=blue>option</span> one </div> <div tabindex=0 class=stylable-select-option> diff --git a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-open-invalidation-ref.html b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-open-invalidation-ref.html index f5b70c495bc..fb609a7247e 100644 --- a/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-open-invalidation-ref.html +++ b/tests/wpt/tests/html/semantics/forms/the-select-element/stylable-select/select-open-invalidation-ref.html @@ -10,7 +10,7 @@ button { <div id=container class=stylable-select-container> <button>button</button> <div id=popover popover=auto anchor=container class=stylable-select-datalist> - <div tabindex=0 class=stylable-select-option>one</div> + <div tabindex=0 class="stylable-select-option selected">one</div> <div class=stylable-select-option>two</div> </div> </div> diff --git a/tests/wpt/tests/html/semantics/forms/the-textarea-element/textarea-update-default-value-in-shadow-crash.html b/tests/wpt/tests/html/semantics/forms/the-textarea-element/textarea-update-default-value-in-shadow-crash.html new file mode 100644 index 00000000000..f2b040434f4 --- /dev/null +++ b/tests/wpt/tests/html/semantics/forms/the-textarea-element/textarea-update-default-value-in-shadow-crash.html @@ -0,0 +1,17 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<script> +"use strict"; + +const p = document.createElement("p"); +document.documentElement.appendChild(p); +const shadowRoot = p.attachShadow({mode: "open"}); +const textarea = document.createElement("textarea"); +shadowRoot.appendChild(textarea); +textarea.defaultValue = "|"; +</script> +</head> +<body></body> +</html> diff --git a/tests/wpt/tests/input-events/input-events-typing.html b/tests/wpt/tests/input-events/input-events-typing.html index a894beea9bd..cff32a44d15 100644 --- a/tests/wpt/tests/input-events/input-events-typing.html +++ b/tests/wpt/tests/input-events/input-events-typing.html @@ -126,6 +126,53 @@ promise_test(async function() { assert_equals(beforeInputEvent.data, inputEvent.data); }, 'It triggers beforeinput and input events on typing BACK_SPACE with pre-existing content'); +promise_test(async function () { + this.add_cleanup(resetRich); + rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>'; + + const expectedResult = [ + // Remove selected text with Backspace + 'deleteContentBackward', + // Remove selected text with Delete + 'deleteContentForward' + ]; + const result = []; + + rich.addEventListener("input", (inputEvent) => { + result.push(inputEvent.inputType); + }); + + const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009'; + + // Click before "content" + await test_driver.click(document.querySelector('#caret')); // Preexisting |content + // Select text to the left + await new test_driver.Actions() + .keyDown(modifierKey) + .keyDown('\uE008') // Shift + .keyDown('\uE012') // Arrow Left + .keyUp('\uE012') + .keyUp('\uE008') + .keyUp(modifierKey) + .send(); // |Preexisting ^content + // Backspace + await test_driver.send_keys(rich, "\uE003"); // |content + // Select text to the right + await new test_driver.Actions() + .keyDown(modifierKey) + .keyDown('\uE008') // Shift + .keyDown('\uE014') // Arrow Right + .keyUp('\uE012') + .keyUp('\uE008') + .keyUp(modifierKey) + .send(); // ^content| + // Delete + await test_driver.send_keys(rich, "\uE017"); // | + + assert_equals(expectedResult.length, result.length); + expectedResult.forEach((er, index) => assert_equals(er, result[index])); +}, 'Input events have correct inputType when selected text is removed with Backspace or Delete'); + promise_test(async function() { this.add_cleanup(resetRich); rich.focus(); diff --git a/tests/wpt/tests/interfaces/FileAPI.idl b/tests/wpt/tests/interfaces/FileAPI.idl index aee0e65dcae..49219fce277 100644 --- a/tests/wpt/tests/interfaces/FileAPI.idl +++ b/tests/wpt/tests/interfaces/FileAPI.idl @@ -20,6 +20,7 @@ interface Blob { [NewObject] ReadableStream stream(); [NewObject] Promise<USVString> text(); [NewObject] Promise<ArrayBuffer> arrayBuffer(); + [NewObject] Promise<Uint8Array> bytes(); }; enum EndingType { "transparent", "native" }; diff --git a/tests/wpt/tests/interfaces/compression.idl b/tests/wpt/tests/interfaces/compression.idl index 7525d7c9847..defe4ba55cd 100644 --- a/tests/wpt/tests/interfaces/compression.idl +++ b/tests/wpt/tests/interfaces/compression.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Compression Streams (https://wicg.github.io/compression/) +// Source: Compression Standard (https://compression.spec.whatwg.org/) enum CompressionFormat { "deflate", diff --git a/tests/wpt/tests/interfaces/credential-management.idl b/tests/wpt/tests/interfaces/credential-management.idl index 75e18319190..94cf6a58614 100644 --- a/tests/wpt/tests/interfaces/credential-management.idl +++ b/tests/wpt/tests/interfaces/credential-management.idl @@ -8,6 +8,7 @@ interface Credential { readonly attribute USVString id; readonly attribute DOMString type; static Promise<boolean> isConditionalMediationAvailable(); + static Promise<undefined> willRequestConditionalCreation(); }; [SecureContext] @@ -45,6 +46,7 @@ enum CredentialMediationRequirement { }; dictionary CredentialCreationOptions { + CredentialMediationRequirement mediation = "optional"; AbortSignal signal; }; diff --git a/tests/wpt/tests/interfaces/cssom-view.idl b/tests/wpt/tests/interfaces/cssom-view.idl index 5c6c2f58a8c..f922bc81486 100644 --- a/tests/wpt/tests/interfaces/cssom-view.idl +++ b/tests/wpt/tests/interfaces/cssom-view.idl @@ -84,10 +84,14 @@ interface Screen { partial interface Document { Element? elementFromPoint(double x, double y); sequence<Element> elementsFromPoint(double x, double y); - CaretPosition? caretPositionFromPoint(double x, double y, ShadowRoot... shadowRoots); + CaretPosition? caretPositionFromPoint(double x, double y, optional CaretPositionFromPointOptions options = {}); readonly attribute Element? scrollingElement; }; +dictionary CaretPositionFromPointOptions { + sequence<ShadowRoot> shadowRoots = []; +}; + [Exposed=Window] interface CaretPosition { readonly attribute Node offsetNode; diff --git a/tests/wpt/tests/interfaces/fenced-frame.idl b/tests/wpt/tests/interfaces/fenced-frame.idl index 9846d4037e9..a3ec8d731bb 100644 --- a/tests/wpt/tests/interfaces/fenced-frame.idl +++ b/tests/wpt/tests/interfaces/fenced-frame.idl @@ -35,6 +35,7 @@ typedef (USVString or FencedFrameConfig) UrnOrConfig; partial interface Navigator { Promise<undefined> deprecatedReplaceInURN( UrnOrConfig urnOrConfig, record<USVString, USVString> replacements); + sequence<USVString> adAuctionComponents(unsigned short numAdComponents); }; enum FenceReportingDestination { diff --git a/tests/wpt/tests/interfaces/html.idl b/tests/wpt/tests/interfaces/html.idl index aad8994b87d..b5bf3573579 100644 --- a/tests/wpt/tests/interfaces/html.idl +++ b/tests/wpt/tests/interfaces/html.idl @@ -77,8 +77,8 @@ partial interface Document { [CEReactions] Document open(optional DOMString unused1, optional DOMString unused2); // both arguments are ignored WindowProxy? open(USVString url, DOMString name, DOMString features); [CEReactions] undefined close(); - [CEReactions] undefined write(HTMLString... text); - [CEReactions] undefined writeln(HTMLString... text); + [CEReactions] undefined write((TrustedHTML or DOMString)... text); + [CEReactions] undefined writeln((TrustedHTML or DOMString)... text); // user interaction readonly attribute WindowProxy? defaultView; @@ -2260,7 +2260,7 @@ interface mixin WindowEventHandlers { attribute EventHandler onunload; }; -typedef (DOMString or Function) TimerHandler; +typedef (DOMString or Function or TrustedScript) TimerHandler; interface mixin WindowOrWorkerGlobalScope { [Replaceable] readonly attribute USVString origin; diff --git a/tests/wpt/tests/interfaces/mediacapture-transform.idl b/tests/wpt/tests/interfaces/mediacapture-transform.idl index 5b2c8fa67a6..1ce35452f0c 100644 --- a/tests/wpt/tests/interfaces/mediacapture-transform.idl +++ b/tests/wpt/tests/interfaces/mediacapture-transform.idl @@ -6,7 +6,7 @@ [Exposed=DedicatedWorker] interface MediaStreamTrackProcessor { constructor(MediaStreamTrackProcessorInit init); - attribute ReadableStream readable; + readonly attribute ReadableStream readable; }; dictionary MediaStreamTrackProcessorInit { diff --git a/tests/wpt/tests/interfaces/mediasession.idl b/tests/wpt/tests/interfaces/mediasession.idl index e6c8e464627..0630f48da4b 100644 --- a/tests/wpt/tests/interfaces/mediasession.idl +++ b/tests/wpt/tests/interfaces/mediasession.idl @@ -26,6 +26,7 @@ enum MediaSessionAction { "seekto", "togglemicrophone", "togglecamera", + "togglescreenshare", "hangup", "previousslide", "nextslide", @@ -47,6 +48,8 @@ interface MediaSession { Promise<undefined> setMicrophoneActive(boolean active); Promise<undefined> setCameraActive(boolean active); + + Promise<undefined> setScreenshareActive(boolean active); }; [Exposed=Window] diff --git a/tests/wpt/tests/interfaces/push-api.idl b/tests/wpt/tests/interfaces/push-api.idl index f582788806c..b16a730b722 100644 --- a/tests/wpt/tests/interfaces/push-api.idl +++ b/tests/wpt/tests/interfaces/push-api.idl @@ -58,6 +58,7 @@ enum PushEncryptionKeyName { interface PushMessageData { ArrayBuffer arrayBuffer(); Blob blob(); + Uint8Array bytes(); any json(); USVString text(); }; diff --git a/tests/wpt/tests/interfaces/saa-non-cookie-storage.idl b/tests/wpt/tests/interfaces/saa-non-cookie-storage.idl new file mode 100644 index 00000000000..6cc0a7887be --- /dev/null +++ b/tests/wpt/tests/interfaces/saa-non-cookie-storage.idl @@ -0,0 +1,45 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Extending Storage Access API (SAA) to non-cookie storage (https://privacycg.github.io/saa-non-cookie-storage/) + +dictionary StorageAccessTypes { + boolean all = false; + boolean cookies = false; + boolean sessionStorage = false; + boolean localStorage = false; + boolean indexedDB = false; + boolean locks = false; + boolean caches = false; + boolean getDirectory = false; + boolean estimate = false; + boolean createObjectURL = false; + boolean revokeObjectURL = false; + boolean BroadcastChannel = false; + boolean SharedWorker = false; +}; + +[Exposed=Window] +interface StorageAccessHandle { + readonly attribute Storage sessionStorage; + readonly attribute Storage localStorage; + readonly attribute IDBFactory indexedDB; + readonly attribute LockManager locks; + readonly attribute CacheStorage caches; + Promise<FileSystemDirectoryHandle> getDirectory(); + Promise<StorageEstimate> estimate(); + DOMString createObjectURL((Blob or MediaSource) obj); + undefined revokeObjectURL(DOMString url); + BroadcastChannel BroadcastChannel(DOMString name); + SharedWorker SharedWorker(USVString scriptURL, optional (DOMString or SharedWorkerOptions) options = {}); +}; + +partial interface Document { + Promise<boolean> hasUnpartitionedCookieAccess(); +}; + +enum SameSiteCookiesType { "all", "none" }; + +dictionary SharedWorkerOptions : WorkerOptions { + SameSiteCookiesType sameSiteCookies; +}; diff --git a/tests/wpt/tests/interfaces/scheduling-apis.idl b/tests/wpt/tests/interfaces/scheduling-apis.idl index 1e84e79cd15..3c858a2db06 100644 --- a/tests/wpt/tests/interfaces/scheduling-apis.idl +++ b/tests/wpt/tests/interfaces/scheduling-apis.idl @@ -9,18 +9,35 @@ enum TaskPriority { "background" }; +enum ContinuationPriority { + "user-blocking", + "user-visible", + "background", + "inherit" +}; + dictionary SchedulerPostTaskOptions { AbortSignal signal; TaskPriority priority; [EnforceRange] unsigned long long delay = 0; }; +enum SchedulerSignalInherit { + "inherit" +}; + +dictionary SchedulerYieldOptions { + (AbortSignal or SchedulerSignalInherit) signal; + ContinuationPriority priority; +}; + callback SchedulerPostTaskCallback = any (); [Exposed=(Window, Worker)] interface Scheduler { Promise<any> postTask(SchedulerPostTaskCallback callback, optional SchedulerPostTaskOptions options = {}); + Promise<undefined> yield(optional SchedulerYieldOptions options = {}); }; [Exposed=(Window, Worker)] diff --git a/tests/wpt/tests/interfaces/turtledove.idl b/tests/wpt/tests/interfaces/turtledove.idl index fdaef0268b6..d4cc8c6bc5d 100644 --- a/tests/wpt/tests/interfaces/turtledove.idl +++ b/tests/wpt/tests/interfaces/turtledove.idl @@ -204,6 +204,7 @@ dictionary BiddingBrowserSignals { required long adComponentsLimit; required unsigned short multiBidLimit; + record<DOMString, DOMString> requestedSize; USVString topLevelSeller; sequence<PreviousWin> prevWinsMs; object wasmHelper; @@ -218,6 +219,7 @@ dictionary ScoringBrowserSignals { required unsigned long biddingDurationMsec; required DOMString bidCurrency; + record<DOMString, DOMString> renderSize; unsigned long dataVersion; sequence<USVString> adComponents; boolean forDebuggingOnlyInCooldownOrLockout = false; diff --git a/tests/wpt/tests/interfaces/webgpu.idl b/tests/wpt/tests/interfaces/webgpu.idl index ef5b9c730ab..4a1b339a002 100644 --- a/tests/wpt/tests/interfaces/webgpu.idl +++ b/tests/wpt/tests/interfaces/webgpu.idl @@ -92,10 +92,10 @@ enum GPUPowerPreference { interface GPUAdapter { [SameObject] readonly attribute GPUSupportedFeatures features; [SameObject] readonly attribute GPUSupportedLimits limits; + [SameObject] readonly attribute GPUAdapterInfo info; readonly attribute boolean isFallbackAdapter; Promise<GPUDevice> requestDevice(optional GPUDeviceDescriptor descriptor = {}); - Promise<GPUAdapterInfo> requestAdapterInfo(); }; dictionary GPUDeviceDescriptor @@ -117,6 +117,7 @@ enum GPUFeatureName { "rg11b10ufloat-renderable", "bgra8unorm-storage", "float32-filterable", + "clip-distances", }; [Exposed=(Window, Worker), SecureContext] diff --git a/tests/wpt/tests/interfaces/webnn.idl b/tests/wpt/tests/interfaces/webnn.idl index 297a2bdbf87..29c88122adc 100644 --- a/tests/wpt/tests/interfaces/webnn.idl +++ b/tests/wpt/tests/interfaces/webnn.idl @@ -90,8 +90,8 @@ interface MLGraphBuilder { // Create an operand for a graph constant. MLOperand constant(MLOperandDescriptor descriptor, ArrayBufferView bufferView); - // Create a single-value operand from the specified number of the specified type. - MLOperand constant(double value, optional MLOperandDataType type = "float32"); + // Create a scalar operand from the specified number of the specified type. + MLOperand constant(MLOperandDataType type, double value); // Compile the graph up to the specified output operands asynchronously. Promise<MLGraph> build(MLNamedOperands outputs); @@ -200,7 +200,7 @@ partial interface MLGraphBuilder { MLOperand greaterOrEqual(MLOperand a, MLOperand b); MLOperand lesser(MLOperand a, MLOperand b); MLOperand lesserOrEqual(MLOperand a, MLOperand b); - MLOperand not(MLOperand a); + MLOperand logicalNot(MLOperand a); }; partial interface MLGraphBuilder { diff --git a/tests/wpt/tests/interfaces/webrtc.idl b/tests/wpt/tests/interfaces/webrtc.idl index da3a6f4645e..de6ba1420ce 100644 --- a/tests/wpt/tests/interfaces/webrtc.idl +++ b/tests/wpt/tests/interfaces/webrtc.idl @@ -434,9 +434,10 @@ dictionary RTCIceParameters { DOMString password; }; -dictionary RTCIceCandidatePair { - required RTCIceCandidate local; - required RTCIceCandidate remote; +[Exposed=Window] +interface RTCIceCandidatePair { + [SameObject] readonly attribute RTCIceCandidate local; + [SameObject] readonly attribute RTCIceCandidate remote; }; enum RTCIceGathererState { diff --git a/tests/wpt/tests/interfaces/webxr-depth-sensing.idl b/tests/wpt/tests/interfaces/webxr-depth-sensing.idl index c44f029436f..b77b59c0ce6 100644 --- a/tests/wpt/tests/interfaces/webxr-depth-sensing.idl +++ b/tests/wpt/tests/interfaces/webxr-depth-sensing.idl @@ -10,7 +10,8 @@ enum XRDepthUsage { enum XRDepthDataFormat { "luminance-alpha", - "float32" + "float32", + "unsigned-short", }; dictionary XRDepthStateInit { @@ -50,6 +51,9 @@ partial interface XRFrame { [Exposed=Window] interface XRWebGLDepthInformation : XRDepthInformation { [SameObject] readonly attribute WebGLTexture texture; + + readonly attribute XRTextureType textureType; + readonly attribute unsigned long? imageIndex; }; partial interface XRWebGLBinding { diff --git a/tests/wpt/tests/lint.ignore b/tests/wpt/tests/lint.ignore index 31ce1ab3a96..170f4136d4b 100644 --- a/tests/wpt/tests/lint.ignore +++ b/tests/wpt/tests/lint.ignore @@ -378,6 +378,7 @@ SET TIMEOUT: css/css-fonts/font-display/font-display-change-ref.html SET TIMEOUT: css/css-fonts/font-display/font-display-feature-policy-01.tentative.html SET TIMEOUT: css/css-fonts/font-display/font-display-feature-policy-02.tentative.html SET TIMEOUT: css/css-fonts/font-display/font-display-preload.html +SET TIMEOUT: css/css-view-transitions/no-painting-while-render-blocked.html SET TIMEOUT: document-policy/font-display/override-to-optional.tentative.html SET TIMEOUT: feature-policy/experimental-features/resources/focus-without-user-activation-iframe-tentative.html SET TIMEOUT: permissions-policy/experimental-features/resources/focus-without-user-activation-iframe-tentative.html diff --git a/tests/wpt/tests/mediacapture-extensions/GUM-backgroundBlur.https.html b/tests/wpt/tests/mediacapture-extensions/GUM-backgroundBlur.https.html index 605a4e08319..5383e088a62 100644 --- a/tests/wpt/tests/mediacapture-extensions/GUM-backgroundBlur.https.html +++ b/tests/wpt/tests/mediacapture-extensions/GUM-backgroundBlur.https.html @@ -90,9 +90,9 @@ Object.keys(constraintSet).forEach(property => { const [videoTrack] = stream.getVideoTracks(); const capabilities = videoTrack.getCapabilities(); - const constraints = {advanced: [{ - [property]: constraintSet[property] - }]}; + const constraints = { + [property]: {exact: constraintSet[property]} + }; const oldSettings = videoTrack.getSettings(); if (supportedConstraints[property] && !(property in capabilities)) { diff --git a/tests/wpt/tests/mediacapture-extensions/GUM-eyeGazeCorrection.https.html b/tests/wpt/tests/mediacapture-extensions/GUM-eyeGazeCorrection.https.html new file mode 100644 index 00000000000..3235b166d84 --- /dev/null +++ b/tests/wpt/tests/mediacapture-extensions/GUM-eyeGazeCorrection.https.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test eye gaze correction support</title> +<link rel="help" href="https://w3c.github.io/mediacapture-extensions/"> +</head> +<body> +<h1 class="instructions">Description</h1> +<p class="instructions">This test checks eye gaze correction support.</p> +<div id='log'></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +const constraintSet = { + eyeGazeCorrection: true +}; + +Object.keys(constraintSet).forEach(property => { + test(t => { + const supportedConstraints = + navigator.mediaDevices.getSupportedConstraints(); + assert_implements_optional( + supportedConstraints[property], + `Optional property ${property} not in supported constraints`); + }, `Test getSupportedConstraints().${property}`); + + promise_test(async t => { + const supportedConstraints = + navigator.mediaDevices.getSupportedConstraints(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + assert_equals(stream.getAudioTracks().length, 0); + assert_equals(stream.getVideoTracks().length, 1); + const [videoTrack] = stream.getVideoTracks(); + + assert_equals(typeof videoTrack.getCapabilities, 'function'); + const capabilities = videoTrack.getCapabilities(); + assert_equals(typeof capabilities, 'object'); + + if (!supportedConstraints[property]) { + assert_false(property in capabilities); + } + + assert_implements_optional( + property in capabilities, + `Optional property ${property} not in capabilities`); + + // Accept [false], [false, true], [true] and [true, false]. + assert_array_equals( + capabilities[property], + capabilities[property].length == 1 + ? [!!capabilities[property][0]] + : [!!capabilities[property][0], !capabilities[property][0]]); + }, `Test getCapabilities().${property}`); + + promise_test(async t => { + const supportedConstraints = + navigator.mediaDevices.getSupportedConstraints(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + assert_equals(stream.getAudioTracks().length, 0); + assert_equals(stream.getVideoTracks().length, 1); + const [videoTrack] = stream.getVideoTracks(); + + const capabilities = videoTrack.getCapabilities(); + + assert_equals(typeof videoTrack.getSettings, 'function'); + const settings = videoTrack.getSettings(); + assert_equals(typeof settings, 'object'); + + if (!supportedConstraints[property] || !(property in capabilities)) + assert_false(property in settings); + + assert_implements_optional( + property in capabilities, + `Optional property ${property} not in capabilities`); + + assert_in_array(settings[property], capabilities[property]); + }, `Test getSettings().${property}`); + + promise_test(async t => { + const supportedConstraints = + navigator.mediaDevices.getSupportedConstraints(); + + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + assert_equals(stream.getAudioTracks().length, 0); + assert_equals(stream.getVideoTracks().length, 1); + const [videoTrack] = stream.getVideoTracks(); + + const capabilities = videoTrack.getCapabilities(); + const constraints = { + [property]: {exact: constraintSet[property]} + }; + const oldSettings = videoTrack.getSettings(); + + if (supportedConstraints[property] && !(property in capabilities)) { + // The user agent supports |constraints| but |videoTrack| is not capable + // to apply them. + await videoTrack.applyConstraints(constraints).then( + () => { + assert_unreached('Unexpected success applying constraints'); + }, + error => {}); + } else { + // The user agent does not support |constraints| and will ignore them or + // the user agent supports |constraints| and |videoTrack| is capable to + // apply them. + await videoTrack.applyConstraints(constraints).then( + () => {}, + error => { + assert_unreached(`Error applying constraints: ${error.message}`); + }); + } + + assert_equals(typeof videoTrack.getConstraints, 'function'); + const appliedConstraints = videoTrack.getConstraints(); + assert_equals(typeof appliedConstraints, 'object'); + const newSettings = videoTrack.getSettings(); + + if (!supportedConstraints[property] || !(property in capabilities)) { + // The user agent does not support |constraints| and ignored them or + // the user agent supports |constraints| but |videoTrack| was not capable + // to apply them. + assert_object_equals(appliedConstraints, {}); + } else { + // The user agent supports |constraints| and |videoTrack| was capable to + // apply them. + assert_object_equals(appliedConstraints, constraints); + } + + if (!supportedConstraints[property] || !(property in capabilities) || + !capabilities[property].includes(constraintSet[property])) { + // The user agent does not support |constraints| and ignored them or + // the user agent supports |constraints| but |videoTrack| was not capable + // to apply them or the user agent supports |constraints| and + // |videoTrack| was capable to apply them but could not satisfy advanced + // constraints and skipped them. + assert_object_equals(newSettings, oldSettings); + } else { + // The user agent supports |constraints| and |videoTrack| was capable to + // apply them and could satisfy advanced constraints. + assert_equals(newSettings[property], constraintSet[property]); + } + }, `Test applyConstraints() with ${property}`); +}); +</script> +</body> +</html> diff --git a/tests/wpt/tests/notifications/fetch-url-resolve.https.window.js b/tests/wpt/tests/notifications/fetch-url-resolve.https.window.js new file mode 100644 index 00000000000..4d2e9940cb8 --- /dev/null +++ b/tests/wpt/tests/notifications/fetch-url-resolve.https.window.js @@ -0,0 +1,32 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/service-workers/service-worker/resources/test-helpers.sub.js +// META: script=resources/helpers.js + +// (Cannot use `global=window,worker` because testdriver only supports window) + +let registration; + +promise_setup(async () => { + await trySettingPermission("granted"); + registration = await getActiveServiceWorker("noop-sw.js"); +}); + +const resolvedUrl = new URL("foo.png", location.href).toString(); + +promise_test(async t => { + const n = new Notification("new Notification", { icon: "foo.png" }); + t.add_cleanup(() => n.close()); + + assert_equals(n.icon, resolvedUrl, "should give a resolved URL"); +}, "new Notification() should give a resolved icon URL"); + +promise_test(async t => { + t.add_cleanup(closeAllNotifications); + + await registration.showNotification("showNotification", { icon: "foo.png" }); + + let notifications = await registration.getNotifications(); + assert_equals(notifications.length, 1, "The list should include one notification"); + assert_equals(notifications[0].icon, resolvedUrl, "should give a resolved URL"); +}, "getNotifications() should give a resolved icon URL"); diff --git a/tests/wpt/tests/permissions-policy/resources/permissions-policy.js b/tests/wpt/tests/permissions-policy/resources/permissions-policy.js index d30d1191d16..d700cb086b3 100644 --- a/tests/wpt/tests/permissions-policy/resources/permissions-policy.js +++ b/tests/wpt/tests/permissions-policy/resources/permissions-policy.js @@ -1,7 +1,6 @@ // Feature test to avoid timeouts function assert_permissions_policy_supported() { - assert_not_equals(document.featurePolicy, undefined, - 'permissions policy is supported'); + assert_true("allow" in HTMLIFrameElement.prototype, 'permissions policy is supported'); } // Tests whether a feature that is enabled/disabled by permissions policy works // as expected. diff --git a/tests/wpt/tests/pointerevents/pointerevent_after_target_removed_from_slot.html b/tests/wpt/tests/pointerevents/pointerevent_after_target_removed_from_slot.html new file mode 100644 index 00000000000..170c1ec2f1c --- /dev/null +++ b/tests/wpt/tests/pointerevents/pointerevent_after_target_removed_from_slot.html @@ -0,0 +1,186 @@ +<!DOCTYPE HTML> +<link rel="help" href="https://w3c.github.io/pointerevents/#firing-events-using-the-pointerevent-interface"> +<title>Enter/leave events fired to parent after child is removed from slot</title> +<meta name="variant" content="?mouse"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="pointerevent_support.js"></script> + +<template id="template"> + <style> + div { + width: 100px; + height: 100px; + } + </style> + <div id="parent"> + <slot id="slot">slot</slot> + </div> +</template> + +<style> + div, my-elem { + width: 100px; + height: 100px; + display: block; + } +</style> + +<my-elem id="host"> + <div id="child">child</div> +</my-elem> +<div id="done">done</div> + +<script> + "use strict"; + + customElements.define( + "my-elem", + class extends HTMLElement { + constructor() { + super(); + let content = document.getElementById("template").content; + const shadowRoot = this.attachShadow({ mode: "open" }); + shadowRoot.appendChild(content.cloneNode(true)); + } + }, + ); + + const pointer_type = location.search.substring(1); + + const shadow_host = document.getElementById("host"); + const parent = shadow_host.shadowRoot.getElementById("parent"); + const slot = parent.firstElementChild; + const slotted_child = document.getElementById("child"); + const done = document.getElementById("done"); + + let event_log = []; + let elem_to_remove; + + function logEvent(e) { + if (e.eventPhase == e.AT_TARGET) { + event_log.push(e.type + "@" + e.target.id); + } + } + + function removeChildFromSlot() { + elem_to_remove.remove(); + event_log.push("(child-removed)"); + } + + function restoreChildInSlot() { + if (!slotted_child.parentElement) { + shadow_host.appendChild(slotted_child); + } + if (!slot.parentElement) { + parent.appendChild(slot); + } + } + + function setup() { + const events = ["pointerover", "pointerout", + "pointerenter", "pointerleave", "pointerdown", "pointerup"]; + let targets = [shadow_host, parent, slot, slotted_child]; + for (let i = 0; i < targets.length; i++) { + events.forEach(event => targets[i].addEventListener(event, logEvent)); + } + } + + function addPromiseTest(remover_event, tested_elem_to_remove, + expected_events) { + assert_true(["slot", "slotted-child"].includes(tested_elem_to_remove), + "Unexpcted tested_elem_to_remove param"); + + const test_name = `Pointer events from ${pointer_type} `+ + `received before/after ${tested_elem_to_remove} removal `+ + `at ${remover_event}`; + + promise_test(async test => { + event_log = []; + elem_to_remove = (tested_elem_to_remove == "slot" ? slot : slotted_child); + + restoreChildInSlot(); + child.addEventListener(remover_event, removeChildFromSlot, + { once: true }); + // TODO(mustaq@chromium.org): It would be more robust if we could remove + // the event listener above through `test.add_cleanup()` but strangely the + // cleanup call fails after the test that removes the slotted-child! This + // happens even if we make the shadow DOM construction dynamic inside this + // `promise_test`!!! + + let done_click_promise = getEvent("click", done); + + let actions = new test_driver.Actions() + .addPointer("TestPointer", pointer_type) + .pointerMove(-30, -30, {origin: shadow_host}) + .pointerDown() + .pointerUp() + .pointerMove(30, 30, {origin: shadow_host}) + .pointerDown() + .pointerUp() + .pointerMove(0, 0, {origin: done}) + .pointerDown() + .pointerUp(); + + await actions.send(); + await done_click_promise; + + assert_equals(event_log.toString(), expected_events.toString(), + "events received"); + }, test_name); + } + + setup(); + + addPromiseTest( + "pointerdown", + "slot", + [ + "pointerover@child", + "pointerenter@host", "pointerenter@parent", "pointerenter@slot", "pointerenter@child", + "pointerdown@child", "(child-removed)", + "pointerout@child", "pointerleave@child", + "pointerover@parent", "pointerup@parent", + "pointerdown@parent", "pointerup@parent", + "pointerout@parent", "pointerleave@parent" + ] + ); + addPromiseTest( + "pointerdown", + "slotted-child", + [ + "pointerover@child", + "pointerenter@host", "pointerenter@parent", "pointerenter@slot", "pointerenter@child", + "pointerdown@child", "(child-removed)", + "pointerleave@slot", "pointerover@parent", "pointerup@parent", + "pointerdown@parent", "pointerup@parent", + "pointerout@parent", "pointerleave@parent" + ] + ); + addPromiseTest( + "pointerup", + "slot", + [ + "pointerover@child", + "pointerenter@host", "pointerenter@parent", "pointerenter@slot", "pointerenter@child", + "pointerdown@child", "pointerup@child", "(child-removed)", + "pointerout@child", "pointerleave@child", + "pointerover@parent", "pointerdown@parent", "pointerup@parent", + "pointerout@parent", "pointerleave@parent" + ] + ); + addPromiseTest( + "pointerup", + "slotted-child", + [ + "pointerover@child", + "pointerenter@host", "pointerenter@parent", "pointerenter@slot", "pointerenter@child", + "pointerdown@child", "pointerup@child", "(child-removed)", + "pointerleave@slot", "pointerover@parent", "pointerdown@parent", "pointerup@parent", + "pointerout@parent", "pointerleave@parent" + ] + ); +</script> diff --git a/tests/wpt/tests/pointerevents/pointerevent_to_slotted_target.html b/tests/wpt/tests/pointerevents/pointerevent_to_slotted_target.html new file mode 100644 index 00000000000..cea318f20d4 --- /dev/null +++ b/tests/wpt/tests/pointerevents/pointerevent_to_slotted_target.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<link rel="help" href="https://w3c.github.io/pointerevents/#firing-events-using-the-pointerevent-interface"> +<title>Enter/leave events fired to parent after child is removed from slot</title> +<meta name="variant" content="?mouse"> +<meta name="variant" content="?touch"> +<meta name="variant" content="?pen"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="pointerevent_support.js"></script> + +<template id="template"> + <style> + div { + width: 100px; + height: 100px; + } + </style> + <div id="parent"> + <slot id="slot">slot</slot> + </div> +</template> + +<style> + div, my-elem { + width: 100px; + height: 100px; + display: block; + } +</style> + +<my-elem id="host"> + <div id="child">child</div> +</my-elem> +<div id="done">done</div> + +<script> + "use strict"; + + customElements.define( + "my-elem", + class extends HTMLElement { + constructor() { + super(); + let content = document.getElementById("template").content; + const shadowRoot = this.attachShadow({ mode: "open" }); + shadowRoot.appendChild(content.cloneNode(true)); + } + }, + ); + + const pointer_type = location.search.substring(1); + + const shadow_host = document.getElementById("host"); + const parent = shadow_host.shadowRoot.getElementById("parent"); + const slot = parent.firstElementChild; + const slotted_child = document.getElementById("child"); + const done = document.getElementById("done"); + + let event_log = []; + + function logEvent(e) { + if (e.eventPhase == e.AT_TARGET) { + event_log.push(e.type + "@" + e.target.id); + } + } + + function setup() { + const events = ["pointerover", "pointerout", + "pointerenter", "pointerleave", "pointerdown", "pointerup"]; + let targets = [shadow_host, parent, slot, slotted_child]; + for (let i = 0; i < targets.length; i++) { + events.forEach(event => targets[i].addEventListener(event, logEvent)); + } + } + + setup(); + + promise_test(async test => { + event_log = []; + + let done_click_promise = getEvent("click", done); + + let actions = new test_driver.Actions() + .addPointer("TestPointer", pointer_type) + .pointerMove(0, 0, {origin: shadow_host}) + .pointerDown() + .pointerUp() + .pointerMove(0, 0, {origin: done}) + .pointerDown() + .pointerUp(); + + await actions.send(); + await done_click_promise; + + const expected_events = [ + "pointerover@child", + "pointerenter@host", "pointerenter@parent", "pointerenter@slot", "pointerenter@child", + "pointerdown@child", "pointerup@child", + "pointerout@child", + "pointerleave@child", "pointerleave@slot", "pointerleave@parent", "pointerleave@host" + ]; + assert_equals(event_log.toString(), expected_events.toString(), + "events received"); + }, `Pointer events from ${pointer_type} to slotted element and shadow-host`); +</script> diff --git a/tests/wpt/tests/preload/preconnect-onerror-event.html b/tests/wpt/tests/preload/preconnect-onerror-event.html index 4ce583d4db8..b2997fed253 100644 --- a/tests/wpt/tests/preload/preconnect-onerror-event.html +++ b/tests/wpt/tests/preload/preconnect-onerror-event.html @@ -1,6 +1,6 @@ <!DOCTYPE html> <html> -<title>Makes sure that preloaded resources trigger the onerror event</title> +<title>Makes sure that preconnected resources do not trigger load or error events</title> <meta name="timeout" content="long"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> @@ -10,6 +10,18 @@ <script> const {HTTP_REMOTE_ORIGIN} = get_host_info(); + /** + * Test workflow: + * - Create preconnect link + * - Wait 200 ms + * - Create preload link + * Event listeners for 'load' and 'error' are attached to both links. + * The first event listener that fires is tested to see which element fired it. + * The event should always be fired by the preload element because per spec, the preconnect link should never fire these events: + * https://html.spec.whatwg.org/#link-type-preconnect + * + * This test assumes that the preload link element fires 'load' and 'error' events correctly, otherwise the test will timeout. + */ function test_preconnect(origin, resource, desc) { promise_test(async t => { const result = await new Promise(async didLoad => { @@ -22,16 +34,16 @@ link.addEventListener('load', () => didLoad({rel, type: 'load'})); link.addEventListener('error', () => didLoad({rel, type: 'error'})); document.head.appendChild(link); - t.step_timeout(() => resolve('timeout'), 200)); + await new Promise(resolve => t.step_timeout(resolve, 200)); } }); assert_equals(result.rel, 'preload'); }, desc); } - test_preconnect(HTTP_REMOTE_ORIGIN, '/preload/resources/dummy.js', 'Preconnect should not fire load events'); - test_preconnect('http://NON-EXISTENT.origin', '/preload/resources/dummy.js', 'Preconnect should not fire error events for non-existent origins'); - test_preconnect('some-scheme://URL', '/preload/resources/dummy.js', 'Preconnect should not fire error events for non-http(s) scheme'); + test_preconnect(HTTP_REMOTE_ORIGIN, '/preload/resources/dummy.js', 'Preconnect should not fire load (or error) events'); + test_preconnect('http://NON-EXISTENT.origin', '/preload/resources/dummy.js', 'Preconnect should not fire error (or load) events for non-existent origins'); + test_preconnect('some-scheme://URL', '/preload/resources/dummy.js', 'Preconnect should not fire error (or load) events for non-http(s) scheme'); </script> </body> </html> diff --git a/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-default.https.sub.html b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-default.https.sub.html new file mode 100644 index 00000000000..81e0c241184 --- /dev/null +++ b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-default.https.sub.html @@ -0,0 +1,41 @@ +<!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="/private-aggregation/resources/util.js"></script> +<script src="/fenced-frame/resources/utils.js"></script> + +<body> +<script> +'use strict'; + +const paa_data = { + enableDebugMode: true, + contributions: [{bucket: 1n, value: 2}] +}; + +const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + +promise_test(async () => { + await VerifyContributeToHistogram(paa_data); +}, 'In a page with default "private-aggregation" permissions policy, ' + + 'initialize the worklet via sharedStorage.worklet.addModule(), and then ' + + 'execute contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/'', paa_data); +}, 'In a page with default "private-aggregation" permissions policy, ' + + 'createWorklet() with same-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/cross_origin, paa_data); +}, 'In a page with default "private-aggregation" permissions policy, ' + + 'createWorklet() with cross-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +</script> +</body> diff --git a/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-none.https.sub.html b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-none.https.sub.html new file mode 100644 index 00000000000..a10efe95c4b --- /dev/null +++ b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-none.https.sub.html @@ -0,0 +1,41 @@ +<!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="/private-aggregation/resources/util.js"></script> +<script src="/fenced-frame/resources/utils.js"></script> + +<body> +<script> +'use strict'; + +const paa_data = { + enableDebugMode: true, + contributions: [{bucket: 1n, value: 2}] +}; + +const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + +promise_test(async () => { + await VerifyContributeToHistogram(paa_data, /*expected_error=*/true); +}, 'In a page with "private-aggregation=()" permissions policy, ' + + 'initialize the worklet via sharedStorage.worklet.addModule(), and then ' + + 'execute contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/'', paa_data, /*expected_error=*/true); +}, 'In a page with "private-aggregation=()" permissions policy, ' + + 'createWorklet() with same-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/cross_origin, paa_data, /*expected_error=*/true); +}, 'In a page with "private-aggregation=()" permissions policy, ' + + 'createWorklet() with cross-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +</script> +</body> diff --git a/tests/wpt/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-none.https.sub.html.headers index 87819101516..87819101516 100644 --- a/tests/wpt/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers +++ b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-none.https.sub.html.headers diff --git a/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html new file mode 100644 index 00000000000..0ec4c6b400d --- /dev/null +++ b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html @@ -0,0 +1,41 @@ +<!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="/private-aggregation/resources/util.js"></script> +<script src="/fenced-frame/resources/utils.js"></script> + +<body> +<script> +'use strict'; + +const paa_data = { + enableDebugMode: true, + contributions: [{bucket: 1n, value: 2}] +}; + +const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + +promise_test(async () => { + await VerifyContributeToHistogram(paa_data); +}, 'In a page with "private-aggregation=(self)" permissions policy, ' + + 'initialize the worklet via sharedStorage.worklet.addModule(), and then ' + + 'execute contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/'', paa_data); +}, 'In a page with "private-aggregation=(self)" permissions policy, ' + + 'createWorklet() with same-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +promise_test(async () => { + await CreateWorkletAndVerifyContributeToHistogram( + /*shared_storage_origin=*/cross_origin, paa_data, /*expected_error=*/true); +}, 'In a page with "private-aggregation=(self)" permissions policy, ' + + 'createWorklet() with cross-origin script, and then execute ' + + 'contributeToHistogram() inside the worklet'); + +</script> +</body> diff --git a/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html.headers b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html.headers new file mode 100644 index 00000000000..c4f9fe899ef --- /dev/null +++ b/tests/wpt/tests/private-aggregation/private-aggregation-permissions-policy-self.https.sub.html.headers @@ -0,0 +1 @@ +Permissions-Policy: private-aggregation=(self) diff --git a/tests/wpt/tests/private-aggregation/resources/shared-storage-helper-module.js.headers b/tests/wpt/tests/private-aggregation/resources/shared-storage-helper-module.js.headers new file mode 100644 index 00000000000..cf3e03e24c7 --- /dev/null +++ b/tests/wpt/tests/private-aggregation/resources/shared-storage-helper-module.js.headers @@ -0,0 +1,2 @@ +Access-Control-Allow-Origin: * +Shared-Storage-Cross-Origin-Worklet-Allowed: ?1 diff --git a/tests/wpt/tests/private-aggregation/resources/util.js b/tests/wpt/tests/private-aggregation/resources/util.js index 24e156446f1..a4e7e905c65 100644 --- a/tests/wpt/tests/private-aggregation/resources/util.js +++ b/tests/wpt/tests/private-aggregation/resources/util.js @@ -1,5 +1,6 @@ -// Execute Private Aggregation functions in shared storage worklet given -// `paa_data`, and expect that success/failure result is `expected_error`. +// In a shared storage worklet initialized via +// sharedStorage.worklet.addModule(), execute Private Aggregation functions +// given `paa_data`, and expect that success/failure result is `expected_error`. async function VerifyContributeToHistogram(paa_data, expected_error) { const ancestor_key = token(); let url0 = generateURL("/shared-storage/resources/frame0.html", @@ -22,3 +23,32 @@ async function VerifyContributeToHistogram(paa_data, expected_error) { assert_equals(result, "frame1_loaded"); } } + +// In a shared storage worklet created from sharedStorage.createWorklet() with +// the given `shared_storage_origin`, execute Private Aggregation functions +// given `paa_data`, and expect that success/failure result is `expected_error`. +// Same-origin script will be used if `shared_storage_origin` is empty. +async function CreateWorkletAndVerifyContributeToHistogram(shared_storage_origin, paa_data, expected_error) { + const ancestor_key = token(); + let url0 = generateURL("/shared-storage/resources/frame0.html", + [ancestor_key]); + let url1 = generateURL("/shared-storage/resources/frame1.html", + [ancestor_key]); + + let worklet = await sharedStorage.createWorklet(shared_storage_origin + + "/private-aggregation/resources/shared-storage-helper-module.js"); + + let select_url_result = await worklet.selectURL( + "contribute-to-histogram", [{url: url0}, {url: url1}], + {data: paa_data, keepAlive: true}); + + attachFencedFrame(select_url_result, 'opaque-ads'); + const result = await nextValueFromServer(ancestor_key); + + if (expected_error) { + assert_equals(result, "frame0_loaded"); + } else { + assert_equals(result, "frame1_loaded"); + } +} + diff --git a/tests/wpt/tests/private-aggregation/shared-storage-permissions-policy-none.https.html b/tests/wpt/tests/private-aggregation/shared-storage-permissions-policy-none.https.html deleted file mode 100644 index 3593ed71ea6..00000000000 --- a/tests/wpt/tests/private-aggregation/shared-storage-permissions-policy-none.https.html +++ /dev/null @@ -1,23 +0,0 @@ -<!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="/private-aggregation/resources/util.js"></script> -<script src="/fenced-frame/resources/utils.js"></script> - -<body> -<script> -'use strict'; - -promise_test(async () => { - const paa_data = { - enableDebugMode: true, - contributions: [{bucket: 1n, value: 2}] - }; - - await VerifyContributeToHistogram(paa_data, /*expected_error=*/true); -}, 'contributeToHistogram() with disabled "private-aggregation" permissions policy'); - -</script> -</body> diff --git a/tests/wpt/tests/resource-timing/content-type.html b/tests/wpt/tests/resource-timing/content-type.html index f6b1db7d9f8..5a09a9114f2 100644 --- a/tests/wpt/tests/resource-timing/content-type.html +++ b/tests/wpt/tests/resource-timing/content-type.html @@ -112,6 +112,16 @@ attribute_test( `content-type should be exposed for iframes having only same origin redirects`); }); +// Content-type for iframes is not exposed if any redirect is not same origin. +var finalDestUrl = `${SAME_ORIGIN}/resource-timing/resources/content-type.py?content_type=text/html`; +var interimDestUrl = `${REMOTE_ORIGIN}/resource-timing/resources/resources/redirect-cors.py?location=` + encodeURIComponent(finalDestUrl); +var destUrl = `${SAME_ORIGIN}/resource-timing/resources/redirect-cors.py?location=` + encodeURIComponent(interimDestUrl); +attribute_test( + load.iframe, new URL(destUrl), + entry => { + assert_equals(entry.contentType, "", + `content-type should not be exposed for iframes with any non-same-origin redirect`); +}); </script> </body> -</html>
\ No newline at end of file +</html> diff --git a/tests/wpt/tests/resources/test/conftest.py b/tests/wpt/tests/resources/test/conftest.py index 7253cac9acf..723087d3184 100644 --- a/tests/wpt/tests/resources/test/conftest.py +++ b/tests/wpt/tests/resources/test/conftest.py @@ -40,11 +40,7 @@ def pytest_collect_file(file_path, path, parent): return test_type = test_type.split(os.path.sep)[1] - # Handle the deprecation of Node construction in pytest6 - # https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent - if hasattr(HTMLItem, "from_parent"): - return HTMLItem.from_parent(parent, filename=str(file_path), test_type=test_type) - return HTMLItem(parent, str(file_path), test_type) + return HTMLFile.from_parent(parent, path=file_path, test_type=test_type) def pytest_configure(config): @@ -144,26 +140,29 @@ def _summarize(actual): return summarized -class HTMLItem(pytest.Item, pytest.Collector): - def __init__(self, parent, filename, test_type): - self.url = parent.session.config.server.url(filename) - self.type = test_type +class HTMLFile(pytest.File): + def __init__(self, test_type=None, **kwargs): + super().__init__(**kwargs) + self.test_type = test_type + + def collect(self): + url = self.session.config.server.url(self.path) # Some tests are reliant on the WPT servers substitution functionality, # so tests must be retrieved from the server rather than read from the # file system directly. - handle = urllib.request.urlopen(self.url, - context=parent.session.config.ssl_context) + handle = urllib.request.urlopen(url, + context=self.parent.session.config.ssl_context) try: markup = handle.read() finally: handle.close() - if test_type not in TEST_TYPES: - raise ValueError('Unrecognized test type: "%s"' % test_type) + if self.test_type not in TEST_TYPES: + raise ValueError('Unrecognized test type: "%s"' % self.test_type) parsed = html5lib.parse(markup, namespaceHTMLElements=False) name = None - self.expected = None + expected = None for element in parsed.iter(): if not name and element.tag == 'title': @@ -172,39 +171,37 @@ class HTMLItem(pytest.Item, pytest.Collector): if element.tag == 'script': if element.attrib.get('id') == 'expected': try: - self.expected = json.loads(element.text) + expected = json.loads(element.text) except ValueError: print("Failed parsing JSON in %s" % filename) raise if not name: raise ValueError('No name found in %s add a <title> element' % filename) - elif self.type == 'functional': - if not self.expected: + elif self.test_type == 'functional': + if not expected: raise ValueError('Functional tests must specify expected report data') - elif self.type == 'unit' and self.expected: + elif self.test_type == 'unit' and expected: raise ValueError('Unit tests must not specify expected report data') - # Ensure that distinct items have distinct fspath attributes. - # This is necessary because pytest has an internal cache keyed on it, - # and only the first test with any given fspath will be run. - # - # This cannot use super(HTMLItem, self).__init__(..) because only the - # Collector constructor takes the fspath argument. - pytest.Item.__init__(self, name, parent) - pytest.Collector.__init__(self, name, parent, fspath=py.path.local(filename)) + yield HTMLItem.from_parent(self, name=name, url=url, expected=expected) + +class HTMLItem(pytest.Item): + def __init__(self, name, parent=None, config=None, session=None, nodeid=None, test_type=None, url=None, expected=None, **kwargs): + super().__init__(name, parent, config, session, nodeid, **kwargs) + + self.test_type = self.parent.test_type + self.url = url + self.expected = expected def reportinfo(self): return self.fspath, None, self.url - def repr_failure(self, excinfo): - return pytest.Collector.repr_failure(self, excinfo) - def runtest(self): - if self.type == 'unit': + if self.test_type == 'unit': self._run_unit_test() - elif self.type == 'functional': + elif self.test_type == 'functional': self._run_functional_test() else: raise NotImplementedError diff --git a/tests/wpt/tests/scroll-to-text-fragment/scroll-to-text-fragment.html b/tests/wpt/tests/scroll-to-text-fragment/scroll-to-text-fragment.html index 73931d4b0e6..91f282f849f 100644 --- a/tests/wpt/tests/scroll-to-text-fragment/scroll-to-text-fragment.html +++ b/tests/wpt/tests/scroll-to-text-fragment/scroll-to-text-fragment.html @@ -47,6 +47,11 @@ let test_cases = [ description: 'Exact text with no context should match text' }, { + fragment: '#:~:text=TEST', + expect_position: 'text', + description: 'Case-insensitive search with no context should match text' + }, + { fragment: '#:~:text=this is a-,test', expect_position: 'text', description: 'Exact text with prefix should match text' diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-flow.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-flow.html new file mode 100644 index 00000000000..d69f95e2afc --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-flow.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>CSS Display: reading-order-items with value flex-flow</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 button:nth-child(3) { + order: -1; +} + +.wrapper { + display: flex; + flex-wrap: wrap; + reading-order-items: flex-flow; +} +</style> + +<div class="test-case" data-expect="C1,A1,B1,D1,E1" + data-description="Flex items in flexbox should follow flex-flow reading order"> + <div class="wrapper" style="width: 200px"> + <button id="A1">Item A</button> + <button id="B1">Item B</button> + <button id="C1">Item C</button> + <button id="D1">Item D</button> + <button id="E1">Item E</button> + </div> +</div> + +<div class="test-case" data-expect="C2,A2,B2,D2,E2" + data-description="Flex items in rtl flexbox should follow flex-flow reading order"> + <div class="wrapper" style="width: 200px; direction: rtl"> + <button id="A2">Item A</button> + <button id="B2">Item B</button> + <button id="C2">Item C</button> + <button id="D2">Item D</button> + <button id="E2">Item E</button> + </div> +</div> + +<div class="test-case" data-expect="C3,A3,B3,D3,E3" + data-description="Flex items in vertical-lr flexbox should follow flex-flow reading order"> + <div class="wrapper" style="height: 200px; writing-mode: vertical-lr"> + <button id="A3">Item A</button> + <button id="B3">Item B</button> + <button id="C3">Item C</button> + <button id="D3">Item D</button> + <button id="E3">Item E</button> + </div> +</div> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-visual-order.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-visual-order.html new file mode 100644 index 00000000000..ce91f9b3c4c --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/flex-visual-order.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value flex-visual</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: flex; + reading-order-items: flex-visual; +} +</style> + +<div class="test-case" data-expect="A,B,C" + data-description="Flex items in flexbox should follow flex-visual reading order"> + <div class="wrapper"> + <button id="A">A</button> + <button id="B">B</button> + <button id="C">C</button> + </div> +</div> + +<div class="test-case" data-expect="C1,B1,A1" + data-description="Flex items in row-reverse flexbox should follow flex-visual reading order"> + <div class="wrapper" style="flex-direction: row-reverse"> + <button id="A1">A</button> + <button id="B1">B</button> + <button id="C1">C</button> + </div> +</div> + +<div class="test-case" data-expect="C2,B2,A2" + data-description="Flex items in row-reverse rtl flexbox should follow flex-visual reading order"> + <div class="wrapper" style="flex-direction: row-reverse; direction: rtl"> + <button id="A2">A</button> + <button id="B2">B</button> + <button id="C2">C</button> + </div> +</div> + +<div class="test-case" data-expect="C3,B3,A3" + data-description="Flex items in row-reverse and vertical-lr flexbox should follow flex-visual reading order"> + <div class="wrapper" style="flex-direction: row-reverse; writing-mode: vertical-lr;"> + <button id="A3">A</button> + <button id="B3">B</button> + <button id="C3">C</button> + </div> +</div> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-columns.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-columns.html new file mode 100644 index 00000000000..f07dc63bc58 --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-columns.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>CSS Display: reading-order-items with value grid-columns</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; + grid-template-columns: repeat(3, 150px); + grid-template-areas: "d b b" + "c c a"; + reading-order-items: grid-columns; +} + +.a { grid-area: a; } +.b { grid-area: b; } +.c { grid-area: c; } +.d { grid-area: d; } + +</style> + +<div class="test-case" data-expect="d1,c1,b1,a1" + data-description="Grid items in grid template columns should follow grid-columns reading order"> + <div class="wrapper"> + <button class="a" id="a1">Item A</button> + <button class="b" id="b1">Item B</button> + <button class="c" id="c1">Item C</button> + <button class="d" id="d1">Item D</button> + </div> +</div> + + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-across-scopes.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-across-scopes.html index d505383e338..81d1b7de539 100644 --- a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-across-scopes.html +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-across-scopes.html @@ -30,7 +30,7 @@ </div> <button id="C" style="order: 3">C</button> </div> -<div> +</div> <script> runFocusTestCases(); diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-rows.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-rows.html new file mode 100644 index 00000000000..f2442746843 --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-rows.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>CSS Display: reading-order-items with value grid-rows</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; + grid-template-columns: repeat(3, 150px); + grid-template-areas: "d b b" + "c c a"; + reading-order-items: grid-rows; + writing-mode: vertical-lr; +} + +.a { grid-area: a; } +.b { grid-area: b; } +.c { grid-area: c; } +.d { grid-area: d; } + +</style> + +<div class="test-case" data-expect="d1,b1,c1,a1" + data-description="Grid items in grid template columns should follow grid-rows reading order"> + <div class="wrapper"> + <button class="a" id="a1">Item A</button> + <button class="b" id="b1">Item B</button> + <button class="c" id="c1">Item C</button> + <button class="d" id="d1">Item D</button> + </div> +</div> + + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus/focus-slot-box-generated-tabindex-0.html b/tests/wpt/tests/shadow-dom/focus/focus-slot-box-generated-tabindex-0.html new file mode 100644 index 00000000000..6406254167c --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus/focus-slot-box-generated-tabindex-0.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>HTML Test: focus - slot with tabindex=0 that generates a box should be focusable</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation"> +<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/shadow-utils.js"></script> +<body> +<div id=host> + <template shadowrootmode=open> + <slot tabindex=0 style="display: inline-block;"></slot> + </template> + Content +</div> +<script> +promise_test(async () => { + const host = document.getElementById("host"); + const slot = host.shadowRoot.querySelector("slot"); + + resetFocus(); + await navigateFocusForward(); + + return assert_equals(slot, host.shadowRoot.activeElement); +}, "slot with tabindex=0 that generates a box should be focusable"); +</script> +</body> diff --git a/tests/wpt/tests/shared-storage/resources/select-url-permissions-policy-helper.html b/tests/wpt/tests/shared-storage/resources/select-url-permissions-policy-helper.html index b70d763ad45..896daa59828 100644 --- a/tests/wpt/tests/shared-storage/resources/select-url-permissions-policy-helper.html +++ b/tests/wpt/tests/shared-storage/resources/select-url-permissions-policy-helper.html @@ -7,7 +7,9 @@ 'use strict'; window.onload = async function() { - if (await IsSharedStorageSelectUrlAllowedByPermissionsPolicy()) { + await sharedStorage.worklet.addModule('/shared-storage/resources/simple-module.js'); + + if (await IsSharedStorageSelectUrlAllowed()) { parent.postMessage({ type: 'availability-result', enabled: true }, '*'); return; } diff --git a/tests/wpt/tests/shared-storage/resources/shared-storage-permissions-policy-helper.html b/tests/wpt/tests/shared-storage/resources/shared-storage-permissions-policy-helper.html index d87092aad1d..dd1bf50cf42 100644 --- a/tests/wpt/tests/shared-storage/resources/shared-storage-permissions-policy-helper.html +++ b/tests/wpt/tests/shared-storage/resources/shared-storage-permissions-policy-helper.html @@ -7,7 +7,7 @@ 'use strict'; window.onload = async function() { - if (await AreSharedStorageMethodsAllowedByPermissionsPolicy()) { + if (await AreRegularSharedStorageMethodsAllowed()) { parent.postMessage({ type: 'availability-result', enabled: true }, '*'); return; } diff --git a/tests/wpt/tests/shared-storage/resources/util.js b/tests/wpt/tests/shared-storage/resources/util.js index 4a7fcc4590f..09eb45c591b 100644 --- a/tests/wpt/tests/shared-storage/resources/util.js +++ b/tests/wpt/tests/shared-storage/resources/util.js @@ -2,87 +2,65 @@ // META: script=/fenced-frame/resources/utils.js 'use strict'; -async function IsSharedStorageSelectUrlAllowedByPermissionsPolicy() { - const errorMessage = 'The \"shared-storage-select-url\" Permissions Policy denied the usage of window.sharedStorage.selectURL().'; - let allowedByPermissionsPolicy = true; +async function IsSharedStorageSelectUrlAllowed() { + let allowed = true; try { - // Run selectURL() with without addModule() and this should always fail. - // Check the error message to distinguish between the permissions policy - // error and the missing addModule() error. await sharedStorage.selectURL("operation", [{url: "1.html"}]); - assert_unreached("did not fail"); } catch (e) { - if (e.message === errorMessage) { - allowedByPermissionsPolicy = false; - } + allowed = false; } - return allowedByPermissionsPolicy; + return allowed; } -// Execute all shared storage methods and capture their errors. Return true if -// the permissions policy allows all of them; return false if the permissions -// policy disallows all of them. Precondition: only these two outcomes are -// possible. -async function AreSharedStorageMethodsAllowedByPermissionsPolicy() { - let permissionsPolicyDeniedCount = 0; - const errorMessage = 'The \"shared-storage\" Permissions Policy denied the method on window.sharedStorage.'; +// Execute all shared storage methods (excluding createWorklet). +// and capture their errors. Return true if all methods succeed. +async function AreRegularSharedStorageMethodsAllowed() { + let deniedCount = 0; try { await window.sharedStorage.worklet.addModule('/shared-storage/resources/simple-module.js'); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } try { - await window.sharedStorage.run('operation'); + await window.sharedStorage.run('operation', {keepAlive: true}); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } try { - // Run selectURL() with without addModule() and this should always fail. - // Check the error message to distinguish between the permissions policy - // error and the missing addModule() error. - await sharedStorage.selectURL("operation", [{url: "1.html"}]); - assert_unreached("did not fail"); + await sharedStorage.selectURL("operation", [{url: "1.html"}], {keepAlive: true}); } catch (e) { - if (e.message === errorMessage) { - ++permissionsPolicyDeniedCount; - } + ++deniedCount; } try { await window.sharedStorage.set('a', 'b'); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } try { await window.sharedStorage.append('a', 'b'); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } try { await window.sharedStorage.clear(); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } try { await window.sharedStorage.delete('a'); } catch (e) { - assert_equals(e.message, errorMessage); - ++permissionsPolicyDeniedCount; + ++deniedCount; } - if (permissionsPolicyDeniedCount === 0) + if (deniedCount === 0) return true; return false; diff --git a/tests/wpt/tests/shared-storage/select-url-permissions-policy-default.tentative.https.sub.html b/tests/wpt/tests/shared-storage/select-url-permissions-policy-default.tentative.https.sub.html index 67911388ec0..a5be825d0e8 100644 --- a/tests/wpt/tests/shared-storage/select-url-permissions-policy-default.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/select-url-permissions-policy-default.tentative.https.sub.html @@ -7,15 +7,34 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/select-url-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'Default permissions policy'; promise_test(async t => { - const allowed = await IsSharedStorageSelectUrlAllowedByPermissionsPolicy(); + await sharedStorage.worklet.addModule(same_origin_script); + const allowed = await IsSharedStorageSelectUrlAllowed(); assert_true(allowed); }, header + ' allows sharedStorage.selectURL() in the current page.'); + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" }); + + await worklet.selectURL("operation", [{url: "1.html"}]); + }, header + ' allows selectURL() on a same-origin worklet'); + + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" }); + + await worklet.selectURL("operation", [{url: "1.html"}]); + }, header + ' allows selectURL() on a cross-origin worklet'); + async_test(t => { test_feature_availability('shared-storage-select-url', t, same_origin_src, expect_feature_available_default); diff --git a/tests/wpt/tests/shared-storage/select-url-permissions-policy-none.tentative.https.sub.html b/tests/wpt/tests/shared-storage/select-url-permissions-policy-none.tentative.https.sub.html index 6820edb0843..6b48036fadb 100644 --- a/tests/wpt/tests/shared-storage/select-url-permissions-policy-none.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/select-url-permissions-policy-none.tentative.https.sub.html @@ -7,15 +7,36 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/select-url-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'permissions policy header shared-storage-select-url=()'; promise_test(async t => { - const allowed = await IsSharedStorageSelectUrlAllowedByPermissionsPolicy(); + await sharedStorage.worklet.addModule('/shared-storage/resources/simple-module.js'); + const allowed = await IsSharedStorageSelectUrlAllowed(); assert_false(allowed); }, header + ' disallows sharedStorage.selectURL() in the current page.'); + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" }); + + return promise_rejects_dom(t, "InvalidAccessError", + worklet.selectURL("operation", [{url: "1.html"}])); + }, header + ' disallows selectURL() on a same-origin worklet'); + + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" }); + + return promise_rejects_dom(t, "InvalidAccessError", + worklet.selectURL("operation", [{url: "1.html"}])); + }, header + ' disallows selectURL() on a cross-origin worklet'); + async_test(t => { test_feature_availability('shared-storage-select-url', t, same_origin_src, expect_feature_unavailable_default); diff --git a/tests/wpt/tests/shared-storage/select-url-permissions-policy-self.tentative.https.sub.html b/tests/wpt/tests/shared-storage/select-url-permissions-policy-self.tentative.https.sub.html index b79bc065c21..71d5653a2d1 100644 --- a/tests/wpt/tests/shared-storage/select-url-permissions-policy-self.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/select-url-permissions-policy-self.tentative.https.sub.html @@ -7,15 +7,35 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/select-url-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'permissions policy header shared-storage-select-url=(self)'; promise_test(async t => { - const allowed = await IsSharedStorageSelectUrlAllowedByPermissionsPolicy(); + await sharedStorage.worklet.addModule(same_origin_script); + const allowed = await IsSharedStorageSelectUrlAllowed(); assert_true(allowed); }, header + ' allows sharedStorage.selectURL() in the current page.'); + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" }); + + await worklet.selectURL("operation", [{url: "1.html"}]); + }, header + ' allows selectURL() on a same-origin worklet'); + + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" }); + + return promise_rejects_dom(t, "InvalidAccessError", + worklet.selectURL("operation", [{url: "1.html"}])); + }, header + ' disallows selectURL() on a cross-origin worklet'); + async_test(t => { test_feature_availability('shared-storage-select-url', t, same_origin_src, expect_feature_available_default); diff --git a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-default.tentative.https.sub.html b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-default.tentative.https.sub.html index 5439df2a068..d0ff76a61ef 100644 --- a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-default.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-default.tentative.https.sub.html @@ -7,15 +7,29 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/shared-storage-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'Default permissions policy'; promise_test(async t => { - const allowed = await AreSharedStorageMethodsAllowedByPermissionsPolicy(); + const allowed = await AreRegularSharedStorageMethodsAllowed(); assert_true(allowed); }, header + ' allows sharedStorage in the current page.'); + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" }); + }, header + ' allows sharedStorage.createWorklet() with same-origin script'); + + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" }); + }, header + ' allows sharedStorage.createWorklet() with cross-origin script'); + async_test(t => { test_feature_availability('shared-storage', t, same_origin_src, expect_feature_available_default); diff --git a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-none.tentative.https.sub.html b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-none.tentative.https.sub.html index 17a4bf1803c..c3cd3b1b478 100644 --- a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-none.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-none.tentative.https.sub.html @@ -7,15 +7,31 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/shared-storage-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'permissions policy header shared-storage=()'; promise_test(async t => { - const allowed = await AreSharedStorageMethodsAllowedByPermissionsPolicy(); + const allowed = await AreRegularSharedStorageMethodsAllowed(); assert_false(allowed); }, header + ' disallows sharedStorage in the current page.'); + promise_test(async t => { + return promise_rejects_dom(t, "InvalidAccessError", + sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" })); + }, header + ' disallows sharedStorage.createWorklet() with same-origin script'); + + promise_test(async t => { + return promise_rejects_dom(t, "InvalidAccessError", + sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" })); + }, header + ' disallows sharedStorage.createWorklet() with cross-origin script'); + async_test(t => { test_feature_availability('shared-storage', t, same_origin_src, expect_feature_unavailable_default); diff --git a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-self.tentative.https.sub.html b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-self.tentative.https.sub.html index 171325cdf8f..9c45e86b67a 100644 --- a/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-self.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/shared-storage-permissions-policy-self.tentative.https.sub.html @@ -7,15 +7,30 @@ <script> 'use strict'; const same_origin_src = '/shared-storage/resources/shared-storage-permissions-policy-helper.html'; - const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + - same_origin_src; + const same_origin_script = '/shared-storage/resources/simple-module.js'; + const cross_origin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const cross_origin_src = cross_origin + same_origin_src; + const cross_origin_script = cross_origin + same_origin_script; const header = 'permissions policy header shared-storage=(self)'; promise_test(async t => { - const allowed = await AreSharedStorageMethodsAllowedByPermissionsPolicy(); + const allowed = await AreRegularSharedStorageMethodsAllowed(); assert_true(allowed); }, header + ' allows sharedStorage in the current page.'); + promise_test(async t => { + const worklet = await sharedStorage.createWorklet( + same_origin_script, + { credentials: "omit" }); + }, header + ' allows sharedStorage.createWorklet() with same-origin script'); + + promise_test(async t => { + return promise_rejects_dom(t, "InvalidAccessError", + sharedStorage.createWorklet( + cross_origin_script, + { credentials: "omit" })); + }, header + ' disallows sharedStorage.createWorklet() with cross-origin script'); + async_test(t => { test_feature_availability('shared-storage', t, same_origin_src, expect_feature_available_default); diff --git a/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-decoding-info.https.html b/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-decoding-info.https.html new file mode 100644 index 00000000000..5f1523701ea --- /dev/null +++ b/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-decoding-info.https.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-media-capabilities-decoding-info.https.html) +// loads the initiator page then the initiator page will prerender itself with +// the `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +// These configurations are copied from +// wpt/media-capabilities/decodingInfo.webrtc.html +const minimalVideoConfiguration = { + contentType: 'video/VP9; profile-level="0"', + width: 800, + height: 600, + bitrate: 3000, + framerate: 24, +}; + +const minimalAudioConfiguration = { + contentType: 'audio/opus', +}; + +const configuration = { + type: 'webrtc', + video: minimalVideoConfiguration, + audio: minimalAudioConfiguration, +}; + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.mediaCapabilities.decodingInfo(configuration), 'navigator.mediaCapabilities.decodingInfo'); +} + +</script> diff --git a/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-encoding-info.https.html b/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-encoding-info.https.html new file mode 100644 index 00000000000..539867824ce --- /dev/null +++ b/tests/wpt/tests/speculation-rules/prerender/resources/media-capabilities-encoding-info.https.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/speculation-rules/prerender/resources/utils.js"></script> +<script src="/speculation-rules/prerender/resources/deferred-promise-utils.js"></script> +<script> + +const params = new URLSearchParams(location.search); + +// The main test page (restriction-media-capabilities-encoding-info.https.html) +// loads the initiator page then the initiator page will prerender itself with +// the `prerendering` parameter. +const isPrerendering = params.has('prerendering'); + +// These configurations are copied from +// wpt/media-capabilities/encodingInfo.webrtc.html +const minimalVideoConfiguration = { + contentType: 'video/VP9; profile-level="0"', + width: 800, + height: 600, + bitrate: 3000, + framerate: 24, +}; + +const minimalAudioConfiguration = { + contentType: 'audio/opus', +}; + +const configuration = { + type: 'webrtc', + video: minimalVideoConfiguration, + audio: minimalAudioConfiguration, +}; + +if (!isPrerendering) { + loadInitiatorPage(); +} else { + const prerenderEventCollector = new PrerenderEventCollector(); + prerenderEventCollector.start( + navigator.mediaCapabilities.encodingInfo(configuration), 'navigator.mediaCapabilities.encodingInfo'); +} + +</script> diff --git a/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-decoding-info.https.html b/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-decoding-info.https.html new file mode 100644 index 00000000000..6bcb63b5978 --- /dev/null +++ b/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-decoding-info.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>MediaCapabilities decodingInfo API is not deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<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="/common/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-capabilities-decoding-info.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.mediaCapabilities.decodingInfo', + prerendering: true + }, + { + event: 'finished waiting navigator.mediaCapabilities.decodingInfo', + prerendering: true + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the MediaCapabilities decodingInfo API should be granted + during prerendering`); +</script> diff --git a/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-encoding-info.https.html b/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-encoding-info.https.html new file mode 100644 index 00000000000..3eb5950ee78 --- /dev/null +++ b/tests/wpt/tests/speculation-rules/prerender/restriction-media-capabilities-encoding-info.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>MediaCapabilities encodingInfo API is not deferred</title> +<meta name="variant" content="?target_hint=_self"> +<meta name="variant" content="?target_hint=_blank"> +<meta name="timeout" content="long"> +<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="/common/utils.js"></script> +<script src="../resources/utils.js"></script> +<script src="resources/utils.js"></script> + +<body> +<script> +setup(() => assertSpeculationRulesIsSupported()); + +promise_test(async t => { + const uid = token(); + const bc = new PrerenderChannel('test-channel', uid); + t.add_cleanup(_ => bc.close()); + + const gotMessage = new Promise(resolve => { + bc.addEventListener('message', e => { + resolve(e.data); + }, { + once: true + }); + }); + const url = `resources/media-capabilities-encoding-info.https.html?uid=${uid}&target_hint=${getTargetHint()}`; + window.open(url, '_blank', 'noopener'); + + const result = await gotMessage; + const expected = [ + { + event: 'started waiting navigator.mediaCapabilities.encodingInfo', + prerendering: true + }, + { + event: 'finished waiting navigator.mediaCapabilities.encodingInfo', + prerendering: true + }, + ]; + assert_equals(result.length, expected.length); + for (let i = 0; i < result.length; i++) { + assert_equals(result[i].event, expected[i].event, `event[${i}]`); + assert_equals(result[i].prerendering, expected[i].prerendering, + `prerendering[${i}]`); + } + + // Send a close signal to PrerenderEventCollector on the prerendered page. + new PrerenderChannel('close', uid).postMessage(''); +}, `The access to the MediaCapabilities encodingInfo API should be granted + during prerendering`); +</script> diff --git a/tests/wpt/tests/storage/opaque-origin.https.window.js b/tests/wpt/tests/storage/opaque-origin.https.window.js index cc1d31fdf2c..b9539760db7 100644 --- a/tests/wpt/tests/storage/opaque-origin.https.window.js +++ b/tests/wpt/tests/storage/opaque-origin.https.window.js @@ -1,4 +1,7 @@ // META: title=StorageManager API and opaque origins +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=resources/helpers.js function load_iframe(src, sandbox) { return new Promise(resolve => { @@ -49,6 +52,10 @@ function make_script(snippet) { '<\/script>'; } +promise_setup(async () => { + await tryDenyingPermission(); +}); + ['navigator.storage.persisted()', 'navigator.storage.estimate()', // persist() can prompt, so make sure we test that last diff --git a/tests/wpt/tests/storage/resources/helpers.js b/tests/wpt/tests/storage/resources/helpers.js new file mode 100644 index 00000000000..2dc5ab00dd1 --- /dev/null +++ b/tests/wpt/tests/storage/resources/helpers.js @@ -0,0 +1,9 @@ +// Try explicitly denying so that persist() won't wait for user prompt +async function tryDenyingPermission() { + try { + await test_driver.set_permission({ name: "persistent-storage" }, "denied"); + } catch { + // Not all implementations support this yet, but some implementations may + // still be able to continue without explicit permission + } +} diff --git a/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js b/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js deleted file mode 100644 index edbe67fae2c..00000000000 --- a/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js +++ /dev/null @@ -1,9 +0,0 @@ -// META: title=StorageManager: result of persist() matches result of persisted() - -promise_test(async t => { - var persistResult = await navigator.storage.persist(); - assert_equals(typeof persistResult, 'boolean', persistResult + ' should be boolean'); - var persistedResult = await navigator.storage.persisted(); - assert_equals(typeof persistedResult, 'boolean', persistedResult + ' should be boolean'); - assert_equals(persistResult, persistedResult); -}, 'navigator.storage.persist() resolves to a value that matches navigator.storage.persisted()'); diff --git a/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.window.js b/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.window.js new file mode 100644 index 00000000000..9a4e0d329fc --- /dev/null +++ b/tests/wpt/tests/storage/storagemanager-persist-persisted-match.https.window.js @@ -0,0 +1,16 @@ +// META: title=StorageManager: result of persist() matches result of persisted() +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=resources/helpers.js + +promise_setup(async () => { + await tryDenyingPermission(); +}); + +promise_test(async t => { + var persistResult = await navigator.storage.persist(); + assert_equals(typeof persistResult, 'boolean', persistResult + ' should be boolean'); + var persistedResult = await navigator.storage.persisted(); + assert_equals(typeof persistedResult, 'boolean', persistedResult + ' should be boolean'); + assert_equals(persistResult, persistedResult); +}, 'navigator.storage.persist() resolves to a value that matches navigator.storage.persisted()'); diff --git a/tests/wpt/tests/storage/storagemanager-persist.https.window.js b/tests/wpt/tests/storage/storagemanager-persist.https.window.js index 13e17a16e14..1bcbefd8a96 100644 --- a/tests/wpt/tests/storage/storagemanager-persist.https.window.js +++ b/tests/wpt/tests/storage/storagemanager-persist.https.window.js @@ -1,4 +1,11 @@ // META: title=StorageManager: persist() +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=resources/helpers.js + +promise_setup(async () => { + await tryDenyingPermission(); +}); promise_test(function() { var promise = navigator.storage.persist(); diff --git a/tests/wpt/tests/svg/import/types-dom-01-b-manual.svg b/tests/wpt/tests/svg/import/types-dom-01-b-manual.svg index 60093ac5420..cf6e3d38f15 100644 --- a/tests/wpt/tests/svg/import/types-dom-01-b-manual.svg +++ b/tests/wpt/tests/svg/import/types-dom-01-b-manual.svg @@ -20,7 +20,7 @@ Note that the values of .getScreenCTM() and .getCTM() can only be tested correctly if they are in the html-based test or the width and height of the root element is explicitly set to 480x360. The methods .getScreenCTM() and .getCTM() are tested from the rotated text element, the method .getBBox(), - .getTransformToElement() is tested between the rotated text and its parent group, the method .getBBox() and + is tested between the rotated text and its parent group, the method .getBBox() and the properties .farthestViewportElement and .nearestViewportElement are tested on the blue circle. </p> </d:testDescription> @@ -40,9 +40,6 @@ .getCTM() for id "rotText": 0.42,0.42,-0.42,0.42,70.00,-60.00 </p> <p> - .getTransformToElement() between id "rotText" and id "parentGroup": 0.42,0.42,-0.42,0.42,0.00,0.00 - </p> - <p> .getBBox() for 'blueCircle': .x=-50,.y=-50,.width=100,.height=100 </p> <p> @@ -70,12 +67,10 @@ document.getElementById("result1").firstChild.nodeValue = ".getScreenCTM(): " + matr.a.toFixed(2) + "," + matr.b.toFixed(2) + "," + matr.c.toFixed(2) + "," + matr.d.toFixed(2) + "," + matr.e.toFixed(2) + "," + matr.f.toFixed(2); var matr = rotText.getCTM(); document.getElementById("result2").firstChild.nodeValue = ".getCTM(): " + matr.a.toFixed(2) + "," + matr.b.toFixed(2) + "," + matr.c.toFixed(2) + "," + matr.d.toFixed(2) + "," + matr.e.toFixed(2) + "," + matr.f.toFixed(2); - var matr = rotText.getTransformToElement(document.getElementById("parentGroup")); - document.getElementById("result3").firstChild.nodeValue = ".getTransformToElement(): " + matr.a.toFixed(2) + "," + matr.b.toFixed(2) + "," + matr.c.toFixed(2) + "," + matr.d.toFixed(2) + "," + matr.e.toFixed(2) + "," + matr.f.toFixed(2); var bbox = blueCircle.getBBox(); - document.getElementById("result4").firstChild.nodeValue = ".getBBox() for 'blueCircle': .x="+bbox.x+",.y="+bbox.y+",.width="+bbox.width+",.height="+bbox.height; - document.getElementById("result5").firstChild.nodeValue = ".farthestViewportElement of blueCircle="+blueCircle.farthestViewportElement.getAttributeNS(null,"id"); - document.getElementById("result6").firstChild.nodeValue = ".nearestViewportElement of blueCircle="+blueCircle.nearestViewportElement.getAttributeNS(null,"id"); + document.getElementById("result3").firstChild.nodeValue = ".getBBox() for 'blueCircle': .x="+bbox.x+",.y="+bbox.y+",.width="+bbox.width+",.height="+bbox.height; + document.getElementById("result4").firstChild.nodeValue = ".farthestViewportElement of blueCircle="+blueCircle.farthestViewportElement.getAttributeNS(null,"id"); + document.getElementById("result5").firstChild.nodeValue = ".nearestViewportElement of blueCircle="+blueCircle.nearestViewportElement.getAttributeNS(null,"id"); } </script> <g font-size="12"> @@ -91,7 +86,6 @@ <text id="result3" x="10" y="240"> </text> <text id="result4" x="10" y="260"> </text> <text id="result5" x="10" y="280"> </text> - <text id="result6" x="10" y="300"> </text> </g> </g> <g font-family="SVGFreeSansASCII,sans-serif" font-size="32"> diff --git a/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-ref.html b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-ref.html new file mode 100644 index 00000000000..5a668b313e5 --- /dev/null +++ b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-ref.html @@ -0,0 +1,12 @@ +<!doctype html> +<style> + @font-face { + font-family: CustomFont; + src: url("/fonts/Ahem.ttf"); + } + svg { font: 25px/1 CustomFont } + body { margin: 0 } +</style> +<svg width="800" height="80"> + <text x="0" y="0">Hello, world!</text> +</svg> diff --git a/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-svg.svg b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-svg.svg new file mode 100644 index 00000000000..536938290f1 --- /dev/null +++ b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image-svg.svg @@ -0,0 +1,11 @@ +<svg width="800" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <style> + @font-face { + font-family: CustomFont; + /* Ahem.ttf */ + src: url("data:font/ttf;base64,AAEAAAALAIAAAwAwT1MvMnNm+H8AAAC8AAAAYGNtYXA5llXuAAABHAAABQxnYXNwABcACQAABigAAAAQZ2x5ZjAqCf0AAAY4AAAaqGhlYWTbUM21AAAg4AAAADZoaGVhBwoENwAAIRgAAAAkaG10eBkGAg0AACE8AAAEWGxvY2G+1Lg3AAAllAAAAi5tYXhwARkACQAAJ8QAAAAgbmFtZbgpxhkAACfkAAAponBvc3RjoVsjAABRiAAAA38AAwPWAZAABQAAArwCigAAAI8CvAKKAAABxQAyAQMAAAIABAAAAAAAAACAAACvEAAgSAAAAAAAAAAAVzNDIABAACD+/wMg/zgAAAMgAMj/EPwHAAD//wMgAyAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEBPgAAAE6AQAABwA6ACYAfgCgAKEAowCkAKUApgCnAKgAqQCqAKsArACtAK4ArwCwALEAswC0ALUAtgC3ALgAuQC6ALsAvAC9AL4AvwDAAMEAwgDDAMUAxgDHAMgAyQDKAMsAzADPANAA0QDSANQA1QDWANcA2ADZANsA3ADdAN4A3wDgAOEA4gDjAOQA5QDmAOcA6ADpAOsA7ADtAO8A8ADxAPIA8wD0APUA9gD3APgA+QD6APwA/QD+AP8BMQFTAXgBkgLGAscCyQLaAtsC3ALdA5QDpQOnA6kDvAPAIAYgCyANIBAgFCAZIBogHiAiICYgMCA6IEQhIiEmIgIiBiIPIhEiEiIZIhoiHiIrIkgiYCJlIvIlyjAAMAdOAE4DTglOXU6MTpRRa1FtU0FW11bbVx9nKGoqbDRwa361kdHwAv7///8AAAAgACgAoAChAKIApAClAKYApwCoAKkAqgCrAKwArQCuAK8AsACxALIAtAC1ALYAtwC4ALkAugC7ALwAvQC+AL8AwADBAMIAwwDEAMYAxwDIAMkAygDLAMwAzQDQANEA0gDTANUA1gDXANgA2QDaANwA3QDeAN8A4ADhAOIA4wDkAOUA5gDnAOgA6QDqAOwA7QDuAPAA8QDyAPMA9AD1APYA9wD4APkA+gD7AP0A/gD/ATEBUgF4AZICxgLHAskC2ALbAtwC3QOUA6UDpwOpA7wDwCACIAkgDCAQIBMgGCAaIBwgICAmIDAgOSBEISIhJiICIgYiDyIRIhIiGSIaIh4iKyJIImAiZCLyJcowADAHTgBOA04JTl1OjE6UUWtRbVNBVtdW21cfZyhqKmw0cGt+tZHR8AD+/////+P/4v/5//T/4P/9/+kAF//d/+L/3//m/+z/6gAX/9kABv/R/9wAFf/V/9r/z//rAAEADf/X/90ADgAMAA3/1f/a/+T/4f/Y/53/xf+c/9//m//a/9v/3//b/+7/lP/c/9n/x/+Q/+7/tP/Y/9X/i//j/+T/p/+J/4f/iP+J/4f/iP+s/4f/iP+G/4f/iP+G/4f/z/+G/4f/hf+G/4f/hf+o/5v/hf+D/4T/xP/F/6H/gf98/1j/P/3t/fX97P3e/eD92P3d/T79b/1r/Sr80/0U4PTg8uDz3//gwuCF4L3gvOC74Ljgr+Cn4J7fwd+t3uLezN7W3tXest6J3s3eyt6+3qXeit6H3fvbJND+0QSzAbMEsvqyrLJ2snGvna+ZrcmqNaopqfKZ55bplNqQooJgbz8Q7wH2AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMACAACABAAAf//AAMAAgB9AAADawMgAAMABwAAMxEhESUhESF9Au79jwH0/gwDIPzgfQImAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gAAAADAAAxIRUhA+j8GMgAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAAAAAPoAyAAAwAAESERIQPo/BgDIPzgAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAQAAAZAD6AJYAAMAABEhFSED6PwYAljIAAABAAABkAPoAlgAAwAAESEVIQPo/BgCWMgAAAEAyP84AZADIAADAAATMxEjyMjIAyD8GAAAAQDI/zgBkAMgAAMAABMzESPIyMgDIPwYAAABAAAAAYAA1YShZl8PPPUACQPoAAAAALNvX1kAAAAAxN2rJAAA/zgD6AMgAAAAAwAAAAAAAAAAAAEAAAMg/zgAAAPoAAAAAAPoAAEAAAAAAAAAAAAAAAAAAAEWA+gAfQAAAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAAAAAAAfQAAAPoAAABTQAAAPoAAACnAAAAyAAAAGQAAAAAAAAD6AAAAAAAAAAAAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAyAPoAMgAAAAUABQAFAAUACEALgA7AEgAVQBiAG8AfACJAJYAowCwAL0AygDXAOQA8QD+AQsBGAElATIBPwFMAVkBZgFzAYABjQGaAacBtAHBAc4B2wHoAfUCAgIPAhwCKQI2AkMCUAJdAmoCdwKEApECngKrArgCxQLSAt8C7AL5AwYDEwMgAy0DOgNHA1QDYQNuA3sDiAOVA6IDrwO8A8kD1gPjA/AD/QQKBBYEIwQwBD0ESgRXBGQEcQR+BIsEmASlBLIEvwTMBNkE5gTzBQAFDQUaBScFNAVBBU4FWwVoBXUFggWPBZwFqQW2BcMF0AXdBeoF9wYEBhEGHgYrBjgGRQZSBl8GbAZ5BoYGkwagBq0GugbHBtQG4QbuBvsHCAcVByIHLwc8B0kHVgdjB3AHfQeKB5cHpAekB7EHvgfLB9gH5QfyB/8IDAgZCCYIMwhACE0IWghnCHQIgQiOCJsIqAi1CMIIzwjcCOkI9gkDCRAJHQkqCTcJRAlRCV4Jawl4CYUJkgmfCawJuQnGCdMJ4AntCfoKBwoUCiEKLgo7CkgKVQpiCm8KfAqJCpYKowqwCr0KygrXCuQK8Qr+CwsLGAslCzILPwtMC1kLZgtzC4ALjQuaC6cLtAvBC84L2wvoC/UMAgwPDBwMKQw2DEMMQwxDDEMMQwxDDEMMQwxDDEMMQwxDDEMMUAxdDGoMdwyEDJEMngyrDLgMxQzSDN8M7Az5DQYNEw0gDS0NOg1HDVQAAAABAAABFgAIAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAJAG2AAAAAAAAAAAB8AAAAAAAAAAAAAEACAHwAAAAAAAAAAIADgH4AAAAAAAAAAMAIgIGAAAAAAAAAAQACAIoAAAAAAAAAAUAGAIwAAAAAAAAAAYACAJIAAAAAAAAAAoQaAJQAAAAAAAAAAsAJBK4AAAAAAAAAA4AUhLcAAEAAAAAAAAA+BMuAAEAAAAAAAEABBQmAAEAAAAAAAIABxQqAAEAAAAAAAMAERQxAAEAAAAAAAQABBRCAAEAAAAAAAUADBRGAAEAAAAAAAYABBRSAAEAAAAAAAoAABRWAAEAAAAAAAsAEhRWAAEAAAAAAA4AKRRoAAEAAAAAABAABBSRAAEAAAAAABEABxSVAAEAAAAAABIABBScAAMAAQQJAAAB8BSgAAMAAQQJAAEACBaQAAMAAQQJAAIADhaYAAMAAQQJAAMAIhamAAMAAQQJAAQACBbIAAMAAQQJAAUAGBbQAAMAAQQJAAYACBboAAMAAQQJAAoQaBbwAAMAAQQJAAsAJCdYAAMAAQQJAA4AUid8AAMAAQQJABAACCfOAAMAAQQJABEADifWAAMAAQQJABIACCfkAFQAaABlACAAQQBoAGUAbQAgAGYAbwBuAHQAIABiAGUAbABvAG4AZwBzACAAdABvACAAdABoAGUAIABwAHUAYgBsAGkAYwAgAGQAbwBtAGEAaQBuAC4AIABJAG4AIABqAHUAcgBpAHMAZABpAGMAdABpAG8AbgBzACAAdABoAGEAdAAgAGQAbwAgAG4AbwB0ACAAcgBlAGMAbwBnAG4AaQB6AGUAIABwAHUAYgBsAGkAYwAgAGQAbwBtAGEAaQBuACAAbwB3AG4AZQByAHMAaABpAHAAIABvAGYAIAB0AGgAZQBzAGUAIABmAGkAbABlAHMALAAgAHQAaABlACAAZgBvAGwAbABvAHcAaQBuAGcAIABDAHIAZQBhAHQAaQB2AGUAIABDAG8AbQBtAG8AbgBzACAAWgBlAHIAbwAgAGQAZQBjAGwAYQByAGEAdABpAG8AbgAgAGEAcABwAGwAaQBlAHMAOgAgAGgAdAB0AHAAOgAvAC8AbABhAGIAcwAuAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcALwBsAGkAYwBlAG4AcwBlAHMALwB6AGUAcgBvAC0AdwBhAGkAdgBlAC8AMQAuADAALwB1AHMALwBsAGUAZwBhAGwAYwBvAGQAZQBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgA1ADAAIABBAGgAZQBtAEEAaABlAG0AVgBlAHIAcwBpAG8AbgAgADEALgA1ADAAQQBoAGUAbQBUAGgAZQAgAEEAaABlAG0AIABmAG8AbgB0ACAAdwBhAHMAIABkAGUAdgBlAGwAbwBwAGUAZAAgAGIAeQAgAFQAbwBkAGQAIABGAGEAaAByAG4AZQByACAAYQBuAGQAIABNAHkAbABlAHMAIABDAC4AIABNAGEAeABmAGkAZQBsAGQAIAB0AG8AIABoAGUAbABwACAAdABlAHMAdAAgAHcAcgBpAHQAZQByAHMAIABkAGUAdgBlAGwAbwBwACAAcAByAGUAZABpAGMAdABhAGIAbABlACAAdABlAHMAdABzAC4AIABUAGgAZQAgAHUAbgBpAHQAcwAgAHAAZQByACAAZQBtACAAaQBzACAAMQAwADAAMAAsACAAdABoAGUAIABhAGQAdgBhAG4AYwBlACAAaQBzACAAOAAwADAALAAgAGEAbgBkACAAdABoAGUAIABkAGUAcwBjAGUAbgB0ACAAaQBzACAAMgAwADAALAAgAHQAaABlAHIAZQBiAHkAIABtAGEAawBpAG4AZwAgAHQAaABlACAAZQBtACAAcwBxAHUAYQByAGUAIABlAHgAYQBjAHQAbAB5ACAAcwBxAHUAYQByAGUALgAgAFQAaABlACAAZwBsAHkAcABoAHMAIABmAG8AcgAgAG0AbwBzAHQAIABjAGgAYQByAGEAYwB0AGUAcgBzACAAaQBzACAAcwBpAG0AcABsAHkAIABhACAAYgBvAHgAIAB3AGgAaQBjAGgAIABmAGkAbABsAHMAIAB0AGgAaQBzACAAcwBxAHUAYQByAGUALgAgAFQAaABlACAAYwBvAGQAZQBwAG8AaQBuAHQAcwAgAG0AYQBwAHAAZQBkACAAdABvACAAdABoAGkAcwAgAGYAdQBsAGwAIABzAHEAdQBhAHIAZQAgAHcAaQB0AGgAIABhACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAIABhAHIAZQAgAHQAaABlACAAZgBvAGwAbABvAHcAaQBuAGcAIAByAGEAbgBnAGUAcwA6ACAAVQArADIAMAAtAFUAKwAyADYALAAgAFUAKwAyADgALQBVACsANgBGACwAIABVACsANwAxAC0AVQArADcARQAsACAAVQArAEEAMAAtAFUAKwBDADgALAAgAFUAKwBDAEEALQBVACsARgBGACwAIABVACsAMQAzADEALAAgAFUAKwAxADUAMgAtAFUAKwAxADUAMwAsACAAVQArADEANwA4ACwAIABVACsAMQA5ADIALAAgAFUAKwAyAEMANgAtAFUAKwAyAEMANwAsACAAVQArADIAQwA5ACwAIABVACsAMgBEADgALQBVACsAMgBEAEQALAAgAFUAKwAzADkANAAsACAAVQArADMAQQA1ACwAIABVACsAMwBBADcALAAgAFUAKwAzAEEAOQAsACAAVQArADMAQgBDACwAIABVACsAMwBDADAALAAgAFUAKwAyADAAMQAzAC0AVQArADIAMAAxADQALAAgAFUAKwAyADAAMQA4AC0AVQArADIAMAAxAEEALAAgAFUAKwAyADAAMQBDAC0AVQArADIAMAAxAEUALAAgAFUAKwAyADAAMgAwAC0AVQArADIAMAAyADIALAAgAFUAKwAyADAAMgA2ACwAIABVACsAMgAwADMAMAAsACAAVQArADIAMAAzADkALQBVACsAMgAwADMAQQAsACAAVQArADIAMAA0ADQALAAgAFUAKwAyADEAMgAyACwAIABVACsAMgAxADIANgAsACAAVQArADIAMgAwADIALAAgAFUAKwAyADIAMAA2ACwAIABVACsAMgAyADAARgAsACAAVQArADIAMgAxADEALQBVACsAMgAyADEAMgAsACAAVQArADIAMgAxADkALQBVACsAMgAyADEAQQAsACAAVQArADIAMgAxAEUALAAgAFUAKwAyADIAMgBCACwAIABVACsAMgAyADQAOAAsACAAVQArADIAMgA2ADAALAAgAFUAKwAyADIANgA0AC0AVQArADIAMgA2ADUALAAgAFUAKwAyADIARgAyACwAIABVACsAMgA1AEMAQQAsACAAVQArADMAMAAwADcALAAgAFUAKwA0AEUAMAAwACwAIABVACsANABFADAAMwAsACAAVQArADQARQAwADkALAAgAFUAKwA0AEUANQBEACwAIABVACsANABFADgAQwAsACAAVQArADQARQA5ADQALAAgAFUAKwA1ADEANgBCACwAIABVACsANQAxADYARAAsACAAVQArADUAMwA0ADEALAAgAFUAKwA1ADYARAA3ACwAIABVACsANQA2AEQAQgAsACAAVQArADUANwAxAEYALAAgAFUAKwA2ADcAMgA4ACwAIABVACsANgBDADMANAAsACAAVQArADcAMAA2AEIALAAgAFUAKwA5ADEARAAxACwAIABVACsARgAwADAAMAAtAFUAKwBGADAAMAAyAC4AIABUAGgAZQAgAGMAbwBkAGUAcABvAGkAbgB0AHMAIAB3AGgAaQBjAGgAIABhAHIAZQAgAG0AYQBwAHAAZQBkACAAdABvACAAcwBvAG0AZQB0AGgAaQBuAGcAIABlAGwAcwBlACAAYQByAGUAIAB0AGgAZQAgAGYAbwBsAGwAbwB3AGkAbgBnADoAIAAiACAAIgAgACgAVQArADIAMAApADoAIABOAG8AIABwAGEAdABoACAAYgB1AHQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAIgBwACIAIAAoAFUAKwA3ADAAKQA6ACAAUABhAHQAaAAgAGgAYQBzACAAMAAgAGEAcwBjAGUAbgB0ACAAYgB1AHQAIABmAHUAbABsACAAZABlAHMAYwBlAG4AdAA7ACAAIgDJACIAIAAoAFUAKwBDADkAKQA6ACAAUABhAHQAaAAgAGgAYQBzACAAMAAgAGQAZQBzAGMAZQBuAHQAIABiAHUAdAAgAGYAdQBsAGwAIABhAHMAYwBlAG4AdAA7ACAATgBvAG4ALQBiAHIAZQBhAGsAaQBuAGcAIABzAHAAYQBjAGUAIAAoAFUAKwBBADAAKQA6ACAATgBvACAAcABhAHQAaAAgAGIAdQB0ACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAOwAgAFoAZQByAG8ALQB3AGkAZAB0AGgAIABuAG8AbgAtAGIAcgBlAGEAawBpAG4AZwAgAHMAcABhAGMAZQAgACgAVQArAEYARQBGAEYAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAMAAgAGEAZAB2AGEAbgBjAGUAOwAgAEUAbgAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADIAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAaABhAGwAZgAgAGEAZAB2AGEAbgBjAGUAOwAgAEUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADMAKQA6ACAATgBvACAAcABhAHQAaAAgAGIAdQB0ACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAOwAgAFQAaAByAGUAZQAtAHAAZQByAC0AZQBtACAAcwBwAGEAYwBlACAAKABVACsAMgAwADAANAApADoAIABOAG8AIABwAGEAdABoACAAYQBuAGQAIABvAG4AZQAgAHQAaABpAHIAZAAgAGEAZAB2AGEAbgBjAGUAOwAgAEYAbwB1AHIALQBwAGUAcgAtAGUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADUAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbwBuAGUAIABxAHUAYQByAHQAZQByACAAYQBkAHYAYQBuAGMAZQA7ACAAUwBpAHgALQBwAGUAcgAtAGUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADYAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbwBuAGUAIABzAGkAeAB0AGgAIABhAGQAdgBhAG4AYwBlADsAIABUAGgAaQBuACAAcwBwAGEAYwBlACAAKABVACsAMgAwADAAOQApADoAIABOAG8AIABwAGEAdABoACAAYQBuAGQAIABvAG4AZQAgAGYAaQBmAHQAaAAgAGEAZAB2AGEAbgBjAGUAOwAgAEgAYQBpAHIAIABzAHAAYQBjAGUAIAAoAFUAKwAyADAAMABBACkAOgAgAE4AbwAgAHAAYQB0AGgAIABhAG4AZAAgAG8AbgBlACAAdABlAG4AdABoACAAYQBkAHYAYQBuAGMAZQA7ACAAWgBlAHIAbwAgAHcAaQBkAHQAaAAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwAEIAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbgBvACAAYQBkAHYAYQBuAGMAZQA7ACAASQBkAGUAbwBnAHIAYQBwAGgAaQBjACAAcwBwAGEAYwBlACAAKABVACsAMwAwADAAMAApADoAIABOAG8AIABwAGEAdABoACAAYgB1AHQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAWgBlAHIAbwAgAHcAaQBkAHQAaAAgAG4AbwBuAC0AagBvAGkAbgBlAHIAIAAoAFUAKwAyADAAMABDACkAOgAgAE4AbwAgAHAAYQB0AGgAIABhAG4AZAAgAG4AbwAgAGEAZAB2AGEAbgBjAGUAOwAgAFoAZQByAG8AIAB3AGkAZAB0AGgAIABqAG8AaQBuAGUAcgAgACgAVQArADIAMAAwAEQAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbgBvACAAYQBkAHYAYQBuAGMAZQA7ACAARwByAGUAZQBrACAAYwBhAHAAaQB0AGEAbAAgAGwAZQB0AHQAZQByACAAQwBoAGkAIAAoAFUAKwAzAEEANwApADoAIABUAGgAaQBuACAAaABvAHIAaQB6AG8AbgB0AGEAbAAgAHMAdAByAGkAcABlACAAYQBuAGQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAImoqACIAIAAoAFUAKwA2AEEAMgBBACkAOgAgAFQAaABpAG4AIABoAG8AcgBpAHoAbwBuAHQAYQBsACAAcwB0AHIAaQBwAGUAIABhAG4AZAAgAGYAdQBsAGwAIABhAGQAdgBhAG4AYwBlADsAIABHAHIAZQBlAGsAIABjAGEAcABpAHQAYQBsACAAbABlAHQAdABlAHIAIABVAHAAcwBpAGwAbwBuACAAKABVACsAMwBBADUAKQA6ACAAVABoAGkAbgAgAHYAZQByAHQAaQBjAGEAbAAgAHMAdAByAGkAcABlACAAYQBuAGQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAIn61ACIAIAAoAFUAKwA3AEUAQgA1ACkAOgAgAFQAaABpAG4AIAB2AGUAcgB0AGkAYwBhAGwAIABzAHQAcgBpAHAAZQAgAGEAbgBkACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUALgBoAHQAdABwADoALwAvAHcAdwB3AC4AdwAzAGMALgBvAHIAZwBoAHQAdABwADoALwAvAGQAZQB2AC4AdwAzAC4AbwByAGcALwBDAFMAUwAvAGYAbwBuAHQAcwAvAGEAaABlAG0ALwBDAE8AUABZAEkATgBHAApUaGUgQWhlbSBmb250IGJlbG9uZ3MgdG8gdGhlIHB1YmxpYyBkb21haW4uIEluIGp1cmlzZGljdGlvbnMgdGhhdCBkbyBub3QgcmVjb2duaXplIHB1YmxpYyBkb21haW4gb3duZXJzaGlwIG9mIHRoZXNlIGZpbGVzLCB0aGUgZm9sbG93aW5nIENyZWF0aXZlIENvbW1vbnMgWmVybyBkZWNsYXJhdGlvbiBhcHBsaWVzOiBodHRwOi8vbGFicy5jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL3plcm8td2FpdmUvMS4wL3VzL2xlZ2FsY29kZUFoZW1SZWd1bGFyVmVyc2lvbiAxLjUwIEFoZW1BaGVtVmVyc2lvbiAxLjUwQWhlbWh0dHA6Ly93d3cudzNjLm9yZ2h0dHA6Ly9kZXYudzMub3JnL0NTUy9mb250cy9haGVtL0NPUFlJTkcKQWhlbVJlZ3VsYXJBaGVtAFQAaABlACAAQQBoAGUAbQAgAGYAbwBuAHQAIABiAGUAbABvAG4AZwBzACAAdABvACAAdABoAGUAIABwAHUAYgBsAGkAYwAgAGQAbwBtAGEAaQBuAC4AIABJAG4AIABqAHUAcgBpAHMAZABpAGMAdABpAG8AbgBzACAAdABoAGEAdAAgAGQAbwAgAG4AbwB0ACAAcgBlAGMAbwBnAG4AaQB6AGUAIABwAHUAYgBsAGkAYwAgAGQAbwBtAGEAaQBuACAAbwB3AG4AZQByAHMAaABpAHAAIABvAGYAIAB0AGgAZQBzAGUAIABmAGkAbABlAHMALAAgAHQAaABlACAAZgBvAGwAbABvAHcAaQBuAGcAIABDAHIAZQBhAHQAaQB2AGUAIABDAG8AbQBtAG8AbgBzACAAWgBlAHIAbwAgAGQAZQBjAGwAYQByAGEAdABpAG8AbgAgAGEAcABwAGwAaQBlAHMAOgAgAGgAdAB0AHAAOgAvAC8AbABhAGIAcwAuAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcALwBsAGkAYwBlAG4AcwBlAHMALwB6AGUAcgBvAC0AdwBhAGkAdgBlAC8AMQAuADAALwB1AHMALwBsAGUAZwBhAGwAYwBvAGQAZQBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgA1ADAAIABBAGgAZQBtAEEAaABlAG0AVgBlAHIAcwBpAG8AbgAgADEALgA1ADAAQQBoAGUAbQBUAGgAZQAgAEEAaABlAG0AIABmAG8AbgB0ACAAdwBhAHMAIABkAGUAdgBlAGwAbwBwAGUAZAAgAGIAeQAgAFQAbwBkAGQAIABGAGEAaAByAG4AZQByACAAYQBuAGQAIABNAHkAbABlAHMAIABDAC4AIABNAGEAeABmAGkAZQBsAGQAIAB0AG8AIABoAGUAbABwACAAdABlAHMAdAAgAHcAcgBpAHQAZQByAHMAIABkAGUAdgBlAGwAbwBwACAAcAByAGUAZABpAGMAdABhAGIAbABlACAAdABlAHMAdABzAC4AIABUAGgAZQAgAHUAbgBpAHQAcwAgAHAAZQByACAAZQBtACAAaQBzACAAMQAwADAAMAAsACAAdABoAGUAIABhAGQAdgBhAG4AYwBlACAAaQBzACAAOAAwADAALAAgAGEAbgBkACAAdABoAGUAIABkAGUAcwBjAGUAbgB0ACAAaQBzACAAMgAwADAALAAgAHQAaABlAHIAZQBiAHkAIABtAGEAawBpAG4AZwAgAHQAaABlACAAZQBtACAAcwBxAHUAYQByAGUAIABlAHgAYQBjAHQAbAB5ACAAcwBxAHUAYQByAGUALgAgAFQAaABlACAAZwBsAHkAcABoAHMAIABmAG8AcgAgAG0AbwBzAHQAIABjAGgAYQByAGEAYwB0AGUAcgBzACAAaQBzACAAcwBpAG0AcABsAHkAIABhACAAYgBvAHgAIAB3AGgAaQBjAGgAIABmAGkAbABsAHMAIAB0AGgAaQBzACAAcwBxAHUAYQByAGUALgAgAFQAaABlACAAYwBvAGQAZQBwAG8AaQBuAHQAcwAgAG0AYQBwAHAAZQBkACAAdABvACAAdABoAGkAcwAgAGYAdQBsAGwAIABzAHEAdQBhAHIAZQAgAHcAaQB0AGgAIABhACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAIABhAHIAZQAgAHQAaABlACAAZgBvAGwAbABvAHcAaQBuAGcAIAByAGEAbgBnAGUAcwA6ACAAVQArADIAMAAtAFUAKwAyADYALAAgAFUAKwAyADgALQBVACsANgBGACwAIABVACsANwAxAC0AVQArADcARQAsACAAVQArAEEAMAAtAFUAKwBDADgALAAgAFUAKwBDAEEALQBVACsARgBGACwAIABVACsAMQAzADEALAAgAFUAKwAxADUAMgAtAFUAKwAxADUAMwAsACAAVQArADEANwA4ACwAIABVACsAMQA5ADIALAAgAFUAKwAyAEMANgAtAFUAKwAyAEMANwAsACAAVQArADIAQwA5ACwAIABVACsAMgBEADgALQBVACsAMgBEAEQALAAgAFUAKwAzADkANAAsACAAVQArADMAQQA1ACwAIABVACsAMwBBADcALAAgAFUAKwAzAEEAOQAsACAAVQArADMAQgBDACwAIABVACsAMwBDADAALAAgAFUAKwAyADAAMQAzAC0AVQArADIAMAAxADQALAAgAFUAKwAyADAAMQA4AC0AVQArADIAMAAxAEEALAAgAFUAKwAyADAAMQBDAC0AVQArADIAMAAxAEUALAAgAFUAKwAyADAAMgAwAC0AVQArADIAMAAyADIALAAgAFUAKwAyADAAMgA2ACwAIABVACsAMgAwADMAMAAsACAAVQArADIAMAAzADkALQBVACsAMgAwADMAQQAsACAAVQArADIAMAA0ADQALAAgAFUAKwAyADEAMgAyACwAIABVACsAMgAxADIANgAsACAAVQArADIAMgAwADIALAAgAFUAKwAyADIAMAA2ACwAIABVACsAMgAyADAARgAsACAAVQArADIAMgAxADEALQBVACsAMgAyADEAMgAsACAAVQArADIAMgAxADkALQBVACsAMgAyADEAQQAsACAAVQArADIAMgAxAEUALAAgAFUAKwAyADIAMgBCACwAIABVACsAMgAyADQAOAAsACAAVQArADIAMgA2ADAALAAgAFUAKwAyADIANgA0AC0AVQArADIAMgA2ADUALAAgAFUAKwAyADIARgAyACwAIABVACsAMgA1AEMAQQAsACAAVQArADMAMAAwADcALAAgAFUAKwA0AEUAMAAwACwAIABVACsANABFADAAMwAsACAAVQArADQARQAwADkALAAgAFUAKwA0AEUANQBEACwAIABVACsANABFADgAQwAsACAAVQArADQARQA5ADQALAAgAFUAKwA1ADEANgBCACwAIABVACsANQAxADYARAAsACAAVQArADUAMwA0ADEALAAgAFUAKwA1ADYARAA3ACwAIABVACsANQA2AEQAQgAsACAAVQArADUANwAxAEYALAAgAFUAKwA2ADcAMgA4ACwAIABVACsANgBDADMANAAsACAAVQArADcAMAA2AEIALAAgAFUAKwA5ADEARAAxACwAIABVACsARgAwADAAMAAtAFUAKwBGADAAMAAyAC4AIABUAGgAZQAgAGMAbwBkAGUAcABvAGkAbgB0AHMAIAB3AGgAaQBjAGgAIABhAHIAZQAgAG0AYQBwAHAAZQBkACAAdABvACAAcwBvAG0AZQB0AGgAaQBuAGcAIABlAGwAcwBlACAAYQByAGUAIAB0AGgAZQAgAGYAbwBsAGwAbwB3AGkAbgBnADoAIAAiACAAIgAgACgAVQArADIAMAApADoAIABOAG8AIABwAGEAdABoACAAYgB1AHQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAIgBwACIAIAAoAFUAKwA3ADAAKQA6ACAAUABhAHQAaAAgAGgAYQBzACAAMAAgAGEAcwBjAGUAbgB0ACAAYgB1AHQAIABmAHUAbABsACAAZABlAHMAYwBlAG4AdAA7ACAAIgDJACIAIAAoAFUAKwBDADkAKQA6ACAAUABhAHQAaAAgAGgAYQBzACAAMAAgAGQAZQBzAGMAZQBuAHQAIABiAHUAdAAgAGYAdQBsAGwAIABhAHMAYwBlAG4AdAA7ACAATgBvAG4ALQBiAHIAZQBhAGsAaQBuAGcAIABzAHAAYQBjAGUAIAAoAFUAKwBBADAAKQA6ACAATgBvACAAcABhAHQAaAAgAGIAdQB0ACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAOwAgAFoAZQByAG8ALQB3AGkAZAB0AGgAIABuAG8AbgAtAGIAcgBlAGEAawBpAG4AZwAgAHMAcABhAGMAZQAgACgAVQArAEYARQBGAEYAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAMAAgAGEAZAB2AGEAbgBjAGUAOwAgAEUAbgAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADIAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAaABhAGwAZgAgAGEAZAB2AGEAbgBjAGUAOwAgAEUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADMAKQA6ACAATgBvACAAcABhAHQAaAAgAGIAdQB0ACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUAOwAgAFQAaAByAGUAZQAtAHAAZQByAC0AZQBtACAAcwBwAGEAYwBlACAAKABVACsAMgAwADAANAApADoAIABOAG8AIABwAGEAdABoACAAYQBuAGQAIABvAG4AZQAgAHQAaABpAHIAZAAgAGEAZAB2AGEAbgBjAGUAOwAgAEYAbwB1AHIALQBwAGUAcgAtAGUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADUAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbwBuAGUAIABxAHUAYQByAHQAZQByACAAYQBkAHYAYQBuAGMAZQA7ACAAUwBpAHgALQBwAGUAcgAtAGUAbQAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwADYAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbwBuAGUAIABzAGkAeAB0AGgAIABhAGQAdgBhAG4AYwBlADsAIABUAGgAaQBuACAAcwBwAGEAYwBlACAAKABVACsAMgAwADAAOQApADoAIABOAG8AIABwAGEAdABoACAAYQBuAGQAIABvAG4AZQAgAGYAaQBmAHQAaAAgAGEAZAB2AGEAbgBjAGUAOwAgAEgAYQBpAHIAIABzAHAAYQBjAGUAIAAoAFUAKwAyADAAMABBACkAOgAgAE4AbwAgAHAAYQB0AGgAIABhAG4AZAAgAG8AbgBlACAAdABlAG4AdABoACAAYQBkAHYAYQBuAGMAZQA7ACAAWgBlAHIAbwAgAHcAaQBkAHQAaAAgAHMAcABhAGMAZQAgACgAVQArADIAMAAwAEIAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbgBvACAAYQBkAHYAYQBuAGMAZQA7ACAASQBkAGUAbwBnAHIAYQBwAGgAaQBjACAAcwBwAGEAYwBlACAAKABVACsAMwAwADAAMAApADoAIABOAG8AIABwAGEAdABoACAAYgB1AHQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAWgBlAHIAbwAgAHcAaQBkAHQAaAAgAG4AbwBuAC0AagBvAGkAbgBlAHIAIAAoAFUAKwAyADAAMABDACkAOgAgAE4AbwAgAHAAYQB0AGgAIABhAG4AZAAgAG4AbwAgAGEAZAB2AGEAbgBjAGUAOwAgAFoAZQByAG8AIAB3AGkAZAB0AGgAIABqAG8AaQBuAGUAcgAgACgAVQArADIAMAAwAEQAKQA6ACAATgBvACAAcABhAHQAaAAgAGEAbgBkACAAbgBvACAAYQBkAHYAYQBuAGMAZQA7ACAARwByAGUAZQBrACAAYwBhAHAAaQB0AGEAbAAgAGwAZQB0AHQAZQByACAAQwBoAGkAIAAoAFUAKwAzAEEANwApADoAIABUAGgAaQBuACAAaABvAHIAaQB6AG8AbgB0AGEAbAAgAHMAdAByAGkAcABlACAAYQBuAGQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAImoqACIAIAAoAFUAKwA2AEEAMgBBACkAOgAgAFQAaABpAG4AIABoAG8AcgBpAHoAbwBuAHQAYQBsACAAcwB0AHIAaQBwAGUAIABhAG4AZAAgAGYAdQBsAGwAIABhAGQAdgBhAG4AYwBlADsAIABHAHIAZQBlAGsAIABjAGEAcABpAHQAYQBsACAAbABlAHQAdABlAHIAIABVAHAAcwBpAGwAbwBuACAAKABVACsAMwBBADUAKQA6ACAAVABoAGkAbgAgAHYAZQByAHQAaQBjAGEAbAAgAHMAdAByAGkAcABlACAAYQBuAGQAIABmAHUAbABsACAAYQBkAHYAYQBuAGMAZQA7ACAAIn61ACIAIAAoAFUAKwA3AEUAQgA1ACkAOgAgAFQAaABpAG4AIAB2AGUAcgB0AGkAYwBhAGwAIABzAHQAcgBpAHAAZQAgAGEAbgBkACAAZgB1AGwAbAAgAGEAZAB2AGEAbgBjAGUALgBoAHQAdABwADoALwAvAHcAdwB3AC4AdwAzAGMALgBvAHIAZwBoAHQAdABwADoALwAvAGQAZQB2AC4AdwAzAC4AbwByAGcALwBDAFMAUwAvAGYAbwBuAHQAcwAvAGEAaABlAG0ALwBDAE8AUABZAEkATgBHAAoAQQBoAGUAbQBSAGUAZwB1AGwAYQByAEEAaABlAG0AAAACAAAAAAAA/3sAFAAAAAAAAAAAAAAAAAAAAAAAAAAAARYAAAECAQMAAwAEAAUABgAHAAgACQALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4AHwAgACEAIgAjACQAJQAmACcAKAApACoAKwAsAC0ALgAvADAAMQAyADMANAA1ADYANwA4ADkAOgA7ADwAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBKAEsATABNAE4ATwBQAFEAUgBTAFQAVQBWAFcAWABZAFoAWwBcAF0AXgBfAGAAYQBiAGMAZABlAGYAZwBoAGkAagBrAGwAbQBuAG8AcABxAHIAcwB0AHUAdgB3AHgAeQB6AHsAfAB9AH4AfwCAAIEAgwCEAIUAhgCIAIkAigCLAI0AjgCQAJEAkwCWAJcAnQCeAKAAoQCiAKMApACpAKoArACtAK4ArwC2ALcAuAC6AL0AwwDHAMgAyQDKAMsAzADNAM4AzwDQANEA0wDUANUA1gDXANgA2QDaANsA3ADdAN4A3wDgAOEA6ADpAOoA6wDsAO0A7gDvAPAA8QDyAPMA9AD1APYBBAEFALAAsQC7AKYAqACfAJsAsgCzAMQAtAC1AMUAggDCAIcAqwDGAL4AvwC8AIwAmACaAJkApQCSAJwAjwCUAJUApwC5ANIAwADBAQYAAgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoBE5VTEwIZ2x5cGgyNDMIZ2x5cGgyMDQIZ2x5cGgyMDUCSFQDREVMB3VuaUZFRkYHdW5pMjAwMgd1bmkyMDAzB3VuaTIwMDQHdW5pMjAwNQd1bmkyMDA2B3VuaTIwMDkHdW5pMjAwQQd1bmkyMDBCB3VuaTMwMDAJYWZpaTYxNjY0B2FmaWkzMDEHdW5pNEUwMAd1bmk0RThDB3VuaTRFMDkHdW5pNTZEQgd1bmk0RTk0B3VuaTUxNkQHdW5pNEUwMwd1bmk1MTZCB3VuaTRFNUQHdW5pNTM0MQd1bmkzMDA3B3VuaTU2RDcHdW5pNzA2Qgd1bmk2QzM0B3VuaTY3MjgHdW5pOTFEMQd1bmk1NzFGB3VuaTAzQTcHdW5pNkEyQQd1bmkwM0E1B3VuaTdFQjUA"); + } + svg { font: 25px/1 CustomFont } + </style> + <text x="0" y="0">Hello, world!</text> +</svg> diff --git a/tests/wpt/tests/svg/text/reftests/text-font-face-load-image.html b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image.html new file mode 100644 index 00000000000..eb9a5fe4357 --- /dev/null +++ b/tests/wpt/tests/svg/text/reftests/text-font-face-load-image.html @@ -0,0 +1,11 @@ +<!doctype html> +<meta charset=utf-8> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1901414"> +<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="text-font-face-load-image-ref.html"> +<style> + img { image-rendering: crisp-edges } + body { margin: 0 } +</style> +<img id="image" src="text-font-face-load-image-svg.svg"> diff --git a/tests/wpt/tests/svg/types/scripted/SVGGraphicsElement.getBBox-04.html b/tests/wpt/tests/svg/types/scripted/SVGGraphicsElement.getBBox-04.html new file mode 100644 index 00000000000..2b5a5eb2bb9 --- /dev/null +++ b/tests/wpt/tests/svg/types/scripted/SVGGraphicsElement.getBBox-04.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<title>SVGGraphicsElement.prototype.getBBox for containers that have children added/removed</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<link rel="help" href="https://svgwg.org/svg2-draft/geometry.html#Sizing"> +<link rel="help" href="https://svgwg.org/svg2-draft/types.html#__svg__SVGGraphicsElement__getBBox"> +<link rel="help" href="https://svgwg.org/svg2-draft/coords.html#BoundingBoxes"> +<svg> + <g> + <rect width="10" height="20"/> + <rect x="20" y="20" width="20" height="20"/> + </g> + <rect x="50" y="50" width="50" height="50"/> +</svg> +<script> +function assert_bbox(element, x, y, width, height, description) { + const bbox = element.getBBox(); + assert_equals(bbox.x, x, `x:${description ?? ''}`); + assert_equals(bbox.y, y, `y:${description ?? ''}`); + assert_equals(bbox.width, width, `width:${description ?? ''}`); + assert_equals(bbox.height, height, `height:${description ?? ''}`); +} + +test(t => { + const svg = document.querySelector('svg'); + const g = document.querySelector('svg > g'); + const rects = Array.from(document.getElementsByTagName('rect')); + assert_bbox(g, 0, 0, 40, 40); + assert_bbox(svg, 0, 0, 100, 100); + + rects[0].remove(); + assert_bbox(g, 20, 20, 20, 20, 'removed rect 1'); + assert_bbox(svg, 20, 20, 80, 80, 'removed rect 1'); + + rects[2].remove(); + assert_bbox(g, 20, 20, 20, 20, 'removed rect 3'); + assert_bbox(svg, 20, 20, 20, 20, 'removed rect 3'); + + rects[1].remove(); + assert_bbox(g, 0, 0, 0, 0, 'removed rect 2'); + assert_bbox(svg, 0, 0, 0, 0, 'removed rect 2'); + + g.appendChild(rects[1]); + assert_bbox(g, 20, 20, 20, 20, 'added rect 2'); + assert_bbox(svg, 20, 20, 20, 20, 'added rect 2'); + + svg.appendChild(rects[2]); + assert_bbox(g, 20, 20, 20, 20, 'added rect 3'); + assert_bbox(svg, 20, 20, 80, 80, 'added rect 3'); + + g.appendChild(rects[0]); + assert_bbox(g, 0, 0, 40, 40, 'added rect 1'); + assert_bbox(svg, 0, 0, 100, 100, 'added rect 1'); +}); +</script> diff --git a/tests/wpt/tests/tools/conftest.py b/tests/wpt/tests/tools/conftest.py index 8d1f585b0d8..56521819507 100644 --- a/tests/wpt/tests/tools/conftest.py +++ b/tests/wpt/tests/tools/conftest.py @@ -17,7 +17,7 @@ settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default" if impl != "PyPy" else "pypy")) -def pytest_ignore_collect(collection_path, path, config): +def pytest_ignore_collect(collection_path, config): # ignore directories which have their own tox.ini assert collection_path != config.rootpath if (collection_path / "tox.ini").is_file(): diff --git a/tests/wpt/tests/tools/docker/requirements.txt b/tests/wpt/tests/tools/docker/requirements.txt index 332fdbdeb1b..6451b3a8004 100644 --- a/tests/wpt/tests/tools/docker/requirements.txt +++ b/tests/wpt/tests/tools/docker/requirements.txt @@ -1,2 +1,2 @@ pyyaml==6.0.1 -requests==2.31.0 +requests==2.32.2 diff --git a/tests/wpt/tests/tools/localpaths.py b/tests/wpt/tests/tools/localpaths.py index a027af852df..ebd3c54e775 100644 --- a/tests/wpt/tests/tools/localpaths.py +++ b/tests/wpt/tests/tools/localpaths.py @@ -11,6 +11,7 @@ sys.path.insert(0, os.path.join(here, "third_party", "atomicwrites")) sys.path.insert(0, os.path.join(here, "third_party", "attrs", "src")) sys.path.insert(0, os.path.join(here, "third_party", "html5lib")) sys.path.insert(0, os.path.join(here, "third_party", "zipp")) +sys.path.insert(0, os.path.join(here, "third_party", "exceptiongroup", "src")) sys.path.insert(0, os.path.join(here, "third_party", "more-itertools")) sys.path.insert(0, os.path.join(here, "third_party", "packaging")) sys.path.insert(0, os.path.join(here, "third_party", "pathlib2")) diff --git a/tests/wpt/tests/tools/requirements_pytest.txt b/tests/wpt/tests/tools/requirements_pytest.txt index 64d38583a2f..75d70d49bd8 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 +pytest==8.2.1 +pytest-cov==5.0.0 hypothesis==6.100.2 diff --git a/tests/wpt/tests/tools/third_party/attrs/.git_archival.txt b/tests/wpt/tests/tools/third_party/attrs/.git_archival.txt new file mode 100644 index 00000000000..8fb235d7045 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ diff --git a/tests/wpt/tests/tools/third_party/attrs/.gitattributes b/tests/wpt/tests/tools/third_party/attrs/.gitattributes new file mode 100644 index 00000000000..ec8c33334fe --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.gitattributes @@ -0,0 +1,5 @@ +# Force LF line endings for text files +* text=auto eol=lf + +# Needed for setuptools-scm-git-archive +.git_archival.txt export-subst diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/CONTRIBUTING.md b/tests/wpt/tests/tools/third_party/attrs/.github/CONTRIBUTING.md index bbdc20f1932..a7e5b014d33 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.github/CONTRIBUTING.md +++ b/tests/wpt/tests/tools/third_party/attrs/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # How To Contribute -First off, thank you for considering contributing to `attrs`! +Thank you for considering contributing to *attrs*! It's people like *you* who make it such a great tool for everyone. This document intends to make contribution more accessible by codifying tribal knowledge and expectations. @@ -16,7 +16,7 @@ Please report any harm to [Hynek Schlawack] in any way you find appropriate. In case you'd like to help out but don't want to deal with GitHub, there's a great opportunity: help your fellow developers on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs)! -The official tag is `python-attrs` and helping out in support frees us up to improve `attrs` instead! +The official tag is `python-attrs` and helping out in support frees us up to improve *attrs* instead! ## Workflow @@ -24,22 +24,151 @@ The official tag is `python-attrs` and helping out in support frees us up to imp - No contribution is too small! Please submit as many fixes for typos and grammar bloopers as you can! - Try to limit each pull request to *one* change only. -- Since we squash on merge, it's up to you how you handle updates to the main branch. - Whether you prefer to rebase on main or merge main into your branch, do whatever is more comfortable for you. +- Since we squash on merge, it's up to you how you handle updates to the `main` branch. + Whether you prefer to rebase on `main` or merge `main` into your branch, do whatever is more comfortable for you. - *Always* add tests and docs for your code. - This is a hard rule; patches with missing tests or documentation can't be merged. + This is a hard rule; patches with missing tests or documentation won't be merged. - Make sure your changes pass our [CI]. You won't get any feedback until it's green unless you ask for it. - For the CI to pass, the coverage must be 100%. If you have problems to test something, open anyway and ask for advice. In some situations, we may agree to add an `# pragma: no cover`. - Once you've addressed review feedback, make sure to bump the pull request with a short note, so we know you're done. -- Don’t break backwards compatibility. +- Don’t break backwards-compatibility. + + +## Local Development Environment + +You can (and should) run our test suite using [*tox*]. +However, you’ll probably want a more traditional environment as well. + +First, create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. +We recommend using the Python version from the `.python-version-default` file in project's root directory. + +If you're using [*direnv*](https://direnv.net), you can automate the creation of a virtual environment with the correct Python version by adding the following `.envrc` to the project root after you've cloned it to your computer: + +```bash +layout python python$(cat .python-version-default) +``` + +If you're using tools that understand `.python-version` files like [*pyenv*](https://github.com/pyenv/pyenv) does, you can make it a link to the `.python-version-default` file. + +--- + +Then, [fork](https://github.com/python-attrs/attrs/fork) the repository on GitHub. + +Clone the fork to your computer: + +```console +$ git clone git@github.com:<your-username>/attrs.git +``` + +Or if you prefer to use Git via HTTPS: + +```console +$ git clone https://github.com/<your-username>/attrs.git +``` + +Then add the *attrs* repository as *upstream* remote: + +```console +$ git remote add -t main -m main --tags upstream https://github.com/python-attrs/attrs.git +``` + +The next step is to sync your local copy with the upstream repository: + +```console +$ git fetch upstream +``` + +This is important to obtain eventually missing tags, which are needed to install the development version later on. +See [#1104](https://github.com/python-attrs/attrs/issues/1104) for more information. + +Change into the newly created directory and after activating a virtual environment install an editable version of *attrs* along with its tests and docs requirements: + +```console +$ cd attrs +$ python -m pip install --upgrade pip wheel # PLEASE don't skip this step +$ python -m pip install -e '.[dev]' +``` + +At this point, + +```console +$ python -m pytest +``` + +should work and pass. +You can *significantly* speed up the test suite by passing `-n auto` to *pytest* which activates [*pytest-xdist*](https://github.com/pytest-dev/pytest-xdist) and takes advantage of all your CPU cores. + +For documentation, you can use: + +```console +$ tox run -e docs-serve +``` + +This will build the documentation, and then watch for changes and rebuild it whenever you save a file. + +To just build the documentation and run doctests, use: + +```console +$ tox run -e docs +``` + +You will find the built documentation in `docs/_build/html`. + + +--- + +To file a pull request, create a new branch on top of the upstream repository's `main` branch: + +```console +$ git fetch upstream +$ git checkout -b my_topical_branch upstream/main +``` + +Make your changes, push them to your fork (the remote *origin*): + +```console +$ git push -u origin +``` + +and publish the PR in GitHub's web interface! + +After your pull request is merged and the branch is no longer needed, delete it: + +```console +$ git checkout main +$ git push --delete origin my_topical_branch && git branch -D my_topical_branch +``` + +Before starting to work on your next pull request, run the following command to sync your local repository with the remote *upstream*: + +```console +$ git fetch upstream -u main:main +``` + +--- + +To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] and its hooks: + +```console +$ pre-commit install +``` + +This is not strictly necessary, because our [*tox*] file contains an environment that runs: + +```console +$ pre-commit run --all-files +``` + +and our CI has integration with [pre-commit.ci](https://pre-commit.ci). +But it's way more comfortable to run it locally and *git* catching avoidable errors. ## Code -- Obey [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/). +- Obey [PEP 8](https://peps.python.org/pep-0008/) and [PEP 257](https://peps.python.org/pep-0257/). We use the `"""`-on-separate-lines style for docstrings: ```python @@ -53,8 +182,8 @@ The official tag is `python-attrs` and helping out in support frees us up to imp """ ``` - If you add or change public APIs, tag the docstring using `.. versionadded:: 16.0.0 WHAT` or `.. versionchanged:: 16.2.0 WHAT`. -- We use [*isort*](https://github.com/PyCQA/isort) to sort our imports, and we use [*Black*](https://github.com/psf/black) with line length of 79 characters to format our code. - As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both – see [*Local Development Environment*](#local-development-environment) below), you won't have to spend any time on formatting your code at all. +- We use [Ruff](https://github.com/astral-sh/ruff) to sort our imports, and we use [Black](https://github.com/psf/black) with line length of 79 characters to format our code. + As long as you run our full [*tox*] suite before committing, or install our [*pre-commit*] hooks (ideally you'll do both – see [*Local Development Environment*](#local-development-environment) above), you won't have to spend any time on formatting your code at all. If you don't, [CI] will catch it for you – but that seems like a waste of your time! @@ -71,7 +200,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp - To run the test suite, all you need is a recent [*tox*]. It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI]. - If you lack some Python versions, you can can always limit the environments like `tox -e py27,py38`, or make it a non-failure using `tox --skip-missing-interpreters`. + If you lack some Python versions, you can can always limit the environments like `tox run -e py38,py39`, or make it a non-failure using `tox run --skip-missing-interpreters`. In that case you should look into [*asdf*](https://asdf-vm.com) or [*pyenv*](https://github.com/pyenv/pyenv), which make it very easy to install many different Python versions in parallel. - Write [good test docstrings](https://jml.io/pages/test-docstrings.html). @@ -81,7 +210,7 @@ The official tag is `python-attrs` and helping out in support frees us up to imp ## Documentation -- Use [semantic newlines] in [*reStructuredText*] files (files ending in `.rst`): +- Use [semantic newlines] in [reStructuredText](https://www.sphinx-doc.org/en/stable/usage/restructuredtext/basics.html) and [Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) files (files ending in `.rst` and `.md`): ```rst This is a sentence. @@ -103,23 +232,23 @@ The official tag is `python-attrs` and helping out in support frees us up to imp First line of new section. ``` -- If you add a new feature, demonstrate its awesomeness on the [examples page](https://github.com/python-attrs/attrs/blob/main/docs/examples.rst)! +- If you add a new feature, demonstrate its awesomeness on the [examples page](https://github.com/python-attrs/attrs/blob/main/docs/examples.md)! ### Changelog If your change is noteworthy, there needs to be a changelog entry so our users can learn about it! -To avoid merge conflicts, we use the [*towncrier*](https://pypi.org/project/towncrier) package to manage our changelog. -*towncrier* uses independent files for each pull request – so called *news fragments* – instead of one monolithic changelog file. -On release, those news fragments are compiled into our [`CHANGELOG.rst`](https://github.com/python-attrs/attrs/blob/main/CHANGELOG.rst). +To avoid merge conflicts, we use the [*Towncrier*](https://pypi.org/project/towncrier) package to manage our changelog. +*towncrier* uses independent *Markdown* files for each pull request – so called *news fragments* – instead of one monolithic changelog file. +On release, those news fragments are compiled into our [`CHANGELOG.md`](https://github.com/python-attrs/attrs/blob/main/CHANGELOG.md). -You don't need to install *towncrier* yourself, you just have to abide by a few simple rules: +You don't need to install *Towncrier* yourself, you just have to abide by a few simple rules: -- For each pull request, add a new file into `changelog.d` with a filename adhering to the `pr#.(change|deprecation|breaking).rst` schema: - For example, `changelog.d/42.change.rst` for a non-breaking change that is proposed in pull request #42. +- For each pull request, add a new file into `changelog.d` with a filename adhering to the `pr#.(change|deprecation|breaking).md` schema: + For example, `changelog.d/42.change.md` for a non-breaking change that is proposed in pull request #42. - As with other docs, please use [semantic newlines] within news fragments. -- Wrap symbols like modules, functions, or classes into double backticks so they are rendered in a `monospace font`. +- Wrap symbols like modules, functions, or classes into backticks so they are rendered in a `monospace font`. - Wrap arguments into asterisks like in docstrings: `Added new argument *an_argument*.` - If you mention functions or other callables, add parentheses at the end of their names: @@ -131,90 +260,30 @@ You don't need to install *towncrier* yourself, you just have to abide by a few + Added `attrs.validators.func()`. + `attrs.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. - If you want to reference multiple issues, copy the news fragment to another filename. - *towncrier* will merge all news fragments with identical contents into one entry with multiple links to the respective pull requests. + *Towncrier* will merge all news fragments with identical contents into one entry with multiple links to the respective pull requests. Example entries: - ```rst - Added ``attrs.validators.func()``. + ```md + Added `attrs.validators.func()`. The feature really *is* awesome. ``` or: - ```rst - ``attrs.func()`` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. + ```md + `attrs.func()` now doesn't crash the Large Hadron Collider anymore when passed the *foobar* argument. The bug really *was* nasty. ``` --- -``tox -e changelog`` will render the current changelog to the terminal if you have any doubts. - - -## Local Development Environment - -You can (and should) run our test suite using [*tox*]. -However, you’ll probably want a more traditional environment as well. -We highly recommend to develop using the latest Python release because we try to take advantage of modern features whenever possible. - -First create a [virtual environment](https://virtualenv.pypa.io/) so you don't break your system-wide Python installation. -It’s out of scope for this document to list all the ways to manage virtual environments in Python, but if you don’t already have a pet way, take some time to look at tools like [*direnv*](https://hynek.me/til/python-project-local-venvs/), [*virtualfish*](https://virtualfish.readthedocs.io/), and [*virtualenvwrapper*](https://virtualenvwrapper.readthedocs.io/). - -Next, get an up to date checkout of the `attrs` repository: - -```console -$ git clone git@github.com:python-attrs/attrs.git -``` - -or if you want to use git via `https`: - -```console -$ git clone https://github.com/python-attrs/attrs.git -``` - -Change into the newly created directory and **after activating your virtual environment** install an editable version of `attrs` along with its tests and docs requirements: - -```console -$ cd attrs -$ pip install --upgrade pip setuptools # PLEASE don't skip this step -$ pip install -e '.[dev]' -``` - -At this point, - -```console -$ python -m pytest -``` - -should work and pass, as should: - -```console -$ cd docs -$ make html -``` - -The built documentation can then be found in `docs/_build/html/`. - -To avoid committing code that violates our style guide, we strongly advise you to install [*pre-commit*] [^dev] hooks: - -```console -$ pre-commit install -``` - -You can also run them anytime (as our tox does) using: - -```console -$ pre-commit run --all-files -``` - -[^dev]: *pre-commit* should have been installed into your virtualenv automatically when you ran `pip install -e '.[dev]'` above. - If *pre-commit* is missing, your probably need to run `pip install -e '.[dev]'` again. +`tox run -e changelog` will render the current changelog to the terminal if you have any doubts. ## Governance -`attrs` is maintained by [team of volunteers](https://github.com/python-attrs) that is always open to new members that share our vision of a fast, lean, and magic-free library that empowers programmers to write better code with less effort. +*attrs* is maintained by [team of volunteers](https://github.com/python-attrs) that is always open to new members that share our vision of a fast, lean, and magic-free library that empowers programmers to write better code with less effort. If you'd like to join, just get a pull request merged and ask to be added in the very same pull request! **The simple rule is that everyone is welcome to review/merge pull requests of others but nobody is allowed to merge their own code.** @@ -225,6 +294,5 @@ If you'd like to join, just get a pull request merged and ask to be added in the [CI]: https://github.com/python-attrs/attrs/actions?query=workflow%3ACI [Hynek Schlawack]: https://hynek.me/about/ [*pre-commit*]: https://pre-commit.com/ -[*tox*]: https://https://tox.wiki/ +[*tox*]: https://tox.wiki/ [semantic newlines]: https://rhodesmill.org/brandon/2012/one-sentence-per-line/ -[*reStructuredText*]: https://www.sphinx-doc.org/en/stable/usage/restructuredtext/basics.html diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/FUNDING.yml b/tests/wpt/tests/tools/third_party/attrs/.github/FUNDING.yml index ef4f2121625..7c250da1e51 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.github/FUNDING.yml +++ b/tests/wpt/tests/tools/third_party/attrs/.github/FUNDING.yml @@ -1,5 +1,3 @@ --- - github: hynek -ko_fi: the_hynek tidelift: "pypi/attrs" diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md b/tests/wpt/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md index 88f6415e96c..e84b6c86ac8 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md +++ b/tests/wpt/tests/tools/third_party/attrs/.github/PULL_REQUEST_TEMPLATE.md @@ -14,6 +14,9 @@ If an item doesn't apply to your pull request, **check it anyway** to make it ap If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing. --> +- [ ] Do **not** open pull requests from your `main` branch – **use a separate branch**! + - There's a ton of footguns waiting if you don't heed this warning. You can still go back to your project, create a branch from your main branch, push it, and open the pull request from the new branch. + - This is not a pre-requisite for your your pull request to be accepted, but **you have been warned**. - [ ] Added **tests** for changed code. Our CI fails if coverage is not 100%. - [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py). @@ -24,9 +27,13 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele - [ ] New functions/classes have to be added to `docs/api.rst` by hand. - [ ] Changes to the signature of `@attr.s()` have to be added by hand too. - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). - Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file. -- [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). + The next version is the second number in the current release + 1. + The first number represents the current year. + So if the current version on PyPI is 22.2.0, the next version is gonna be 22.3.0. + If the next version is the first in the new year, it'll be 23.1.0. +- [ ] Documentation in `.rst` and `.md` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). - [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d). +- [ ] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you. <!-- If you have *any* questions to *any* of the points above, just **submit and ask**! diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/SECURITY.md b/tests/wpt/tests/tools/third_party/attrs/.github/SECURITY.md index 5e565ec19cd..1b8e14cf1ef 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.github/SECURITY.md +++ b/tests/wpt/tests/tools/third_party/attrs/.github/SECURITY.md @@ -1,2 +1,21 @@ +# Security Policy + +## Supported Versions + +We are following [*CalVer*](https://calver.org) with generous backwards-compatibility guarantees. +Therefore we only support the latest version. + +Put simply, you shouldn't ever be afraid to upgrade as long as you're only using our public APIs. +Whenever there is a need to break compatibility, it is announced in the changelog, and raises a `DeprecationWarning` for a year (if possible) before it's finally really broken. + +> **Warning** +> The structure of the `attrs.Attribute` class is exempt from this rule. +> It *will* change in the future, but since it should be considered read-only, that shouldn't matter. +> +> However if you intend to build extensions on top of *attrs* you have to anticipate that. + + +## Reporting a Vulnerability + To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/dependabot.yml b/tests/wpt/tests/tools/third_party/attrs/.github/dependabot.yml new file mode 100644 index 00000000000..fd898955fba --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/build-docset.yml b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/build-docset.yml new file mode 100644 index 00000000000..ec0230d080c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/build-docset.yml @@ -0,0 +1,34 @@ +--- +name: Build docset + +on: + push: + tags: ["*"] + workflow_dispatch: + +env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_PYTHON_VERSION_WARNING: "1" + +permissions: + contents: read + +jobs: + docset: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - run: python -Im pip install tox + + - run: python -Im tox run -e docset + + - uses: actions/upload-artifact@v3 + with: + name: docset + path: attrs.tgz diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/ci.yml b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/ci.yml new file mode 100644 index 00000000000..ca816aa601c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/ci.yml @@ -0,0 +1,178 @@ +--- +name: CI + +on: + merge_group: + push: + branches: [main] + tags: ["*"] + pull_request: + branches: [main] + workflow_dispatch: + +env: + FORCE_COLOR: "1" # Make tools pretty. + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_PYTHON_VERSION_WARNING: "1" + # Use oldest version used in doctests / examples. + SETUPTOOLS_SCM_PRETEND_VERSION: "19.2.0" + +permissions: {} + +jobs: + tests: + name: Tests & Mypy on ${{ matrix.python-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + # - "pypy-3.7" + - "pypy-3.8" + - "pypy-3.9" + - "pypy-3.10" + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + cache: pip + + - name: Prepare tox + run: | + V=${{ matrix.python-version }} + + if [[ "$V" = pypy-* ]]; then + V=pypy3 + IS_PYPY=1 + else + V=py$(echo $V | tr -d .) + IS_PYPY=0 + fi + + echo IS_PYPY=$IS_PYPY >>$GITHUB_ENV + echo TOX_PYTHON=$V >>$GITHUB_ENV + + python -Im pip install tox + + - run: python -Im tox run -e ${{ env.TOX_PYTHON }}-tests + - run: python -Im tox run -e ${{ env.TOX_PYTHON }}-mypy + if: env.IS_PYPY == '0' && matrix.python-version != '3.7' + + - name: Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: coverage-data + path: .coverage.* + if-no-files-found: ignore + + coverage: + name: Combine & check coverage. + runs-on: ubuntu-latest + needs: tests + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version-file: .python-version-default + cache: pip + + - name: Download coverage data + uses: actions/download-artifact@v3 + with: + name: coverage-data + + - name: Combine coverage & fail if it's <100%. + run: | + python -Im pip install coverage[toml] + + python -Im coverage combine + python -Im coverage html --skip-covered --skip-empty + + # Report and write to summary. + python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY + + # Report again and fail if under 100%. + python -Im coverage report --fail-under=100 + + - name: Upload HTML report if check failed. + uses: actions/upload-artifact@v3 + with: + name: html-report + path: htmlcov + if: ${{ failure() }} + + docs: + name: Build docs & run doctests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + # Keep in sync with tox/docs and .readthedocs.yaml. + python-version: "3.12" + cache: pip + + - run: python -Im pip install tox + - run: python -Im tox run -e docs,changelog + + pyright: + name: Check types using pyright + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version-file: .python-version-default + cache: pip + + - run: python -Im pip install tox + - run: python -Im tox run -e pyright + + install-dev: + name: Verify dev env + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version-file: .python-version-default + cache: pip + + - name: Install in dev mode & import + run: | + python -Im pip install -e .[dev] + python -Ic 'import attr; print(attr.__version__)' + python -Ic 'import attrs; print(attrs.__version__)' + + # Ensure everything required is passing for branch protection. + required-checks-pass: + if: always() + + needs: + - coverage + - docs + - install-dev + - pyright + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/codeql-analysis.yml b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..f75fafa5be3 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/codeql-analysis.yml @@ -0,0 +1,35 @@ +--- +name: CodeQL + +on: + schedule: + - cron: "30 22 * * 4" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [python] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/main.yml b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/main.yml deleted file mode 100644 index f38fd915096..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/main.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -name: CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - workflow_dispatch: - -env: - FORCE_COLOR: "1" # Make tools pretty. - TOX_TESTENV_PASSENV: FORCE_COLOR - PYTHON_LATEST: "3.10" - - -jobs: - tests: - name: tox on ${{ matrix.python-version }} - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-2.7", "pypy-3.7", "pypy-3.8"] - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: "Install dependencies" - run: | - python -VV - python -m site - python -m pip install --upgrade pip setuptools wheel - python -m pip install --upgrade virtualenv tox tox-gh-actions - - - run: "python -m tox" - - - name: Upload coverage data - uses: "actions/upload-artifact@v2" - with: - name: coverage-data - path: ".coverage.*" - if-no-files-found: ignore - - - coverage: - runs-on: ubuntu-latest - needs: tests - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - # Use latest Python, so it understands all syntax. - python-version: ${{env.PYTHON_LATEST}} - - - run: python -m pip install --upgrade coverage[toml] - - - name: Download coverage data - uses: actions/download-artifact@v2 - with: - name: coverage-data - - - name: Combine coverage and fail if it's <100%. - run: | - python -m coverage combine - python -m coverage html --skip-covered --skip-empty - python -m coverage report --fail-under=100 - - - name: Upload HTML report if check failed. - uses: actions/upload-artifact@v2 - with: - name: html-report - path: htmlcov - if: ${{ failure() }} - - - package: - name: Build & verify package - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{env.PYTHON_LATEST}} - - - run: python -m pip install build twine check-wheel-contents - - run: python -m build --sdist --wheel . - - run: ls -l dist - - run: check-wheel-contents dist/*.whl - - name: Check long_description - run: python -m twine check dist/* - - - install-dev: - name: Verify dev env - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: ["ubuntu-latest", "windows-latest"] - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{env.PYTHON_LATEST}} - - run: python -m pip install -e .[dev] - - run: python -c 'import attr; print(attr.__version__)' diff --git a/tests/wpt/tests/tools/third_party/attrs/.github/workflows/pypi-package.yml b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/pypi-package.yml new file mode 100644 index 00000000000..8495480c1bc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.github/workflows/pypi-package.yml @@ -0,0 +1,68 @@ +--- +name: Build & maybe upload PyPI package + +on: + push: + branches: [main] + tags: ["*"] + pull_request: + branches: [main] + release: + types: + - published + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + # Always build & lint package. + build-package: + name: Build & verify package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: hynek/build-and-inspect-python-package@v2 + + # Upload to Test PyPI on every commit on main. + release-test-pypi: + name: Publish in-dev package to test.pypi.org + environment: release-test-pypi + if: github.repository_owner == 'python-attrs' && github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: build-package + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Upload package to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + # Upload to real PyPI on GitHub Releases. + release-pypi: + name: Publish released package to pypi.org + environment: release-pypi + if: github.repository_owner == 'python-attrs' && github.event.action == 'published' + runs-on: ubuntu-latest + needs: build-package + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/tests/wpt/tests/tools/third_party/attrs/.gitignore b/tests/wpt/tests/tools/third_party/attrs/.gitignore index d054dc6267d..b58afd704e5 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.gitignore +++ b/tests/wpt/tests/tools/third_party/attrs/.gitignore @@ -1,13 +1,22 @@ *.egg-info *.pyc +.DS_Store .cache .coverage* +.direnv +.envrc .hypothesis .mypy_cache .pytest_cache .tox +.vscode +.venv* build dist -docs/_build/ +docs/_build htmlcov -pip-wheel-metadata +tmp* +attrs.docset +attrs.tgz +Justfile +t.py diff --git a/tests/wpt/tests/tools/third_party/attrs/.pre-commit-config.yaml b/tests/wpt/tests/tools/third_party/attrs/.pre-commit-config.yaml index a913b068f52..df183144316 100644 --- a/tests/wpt/tests/tools/third_party/attrs/.pre-commit-config.yaml +++ b/tests/wpt/tests/tools/third_party/attrs/.pre-commit-config.yaml @@ -4,40 +4,26 @@ ci: repos: - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 23.12.1 hooks: - id: black - exclude: tests/test_pattern_matching.py - language_version: python3.10 - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.9 hooks: - - id: isort - additional_dependencies: [toml] - files: \.py$ - language_version: python3.10 - - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - language_version: python3.10 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/econchick/interrogate rev: 1.5.0 hooks: - id: interrogate - exclude: tests/test_pattern_matching.py args: [tests] - language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: debug-statements - language_version: python3.10 - id: check-toml - id: check-yaml diff --git a/tests/wpt/tests/tools/third_party/attrs/.python-version-default b/tests/wpt/tests/tools/third_party/attrs/.python-version-default new file mode 100644 index 00000000000..e4fba218358 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.python-version-default @@ -0,0 +1 @@ +3.12 diff --git a/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yaml b/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yaml new file mode 100644 index 00000000000..53bc38f7ee3 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yaml @@ -0,0 +1,15 @@ +--- +version: 2 + +build: + os: ubuntu-22.04 + tools: + # Keep version in sync with tox.ini/docs and ci.yml/docs. + python: "3.12" + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yml b/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yml deleted file mode 100644 index d335c40d567..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/.readthedocs.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -version: 2 -formats: all - -build: - os: ubuntu-20.04 - tools: - # Keep version in sync with tox.ini (docs and gh-actions). - python: "3.10" - -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/tests/wpt/tests/tools/third_party/attrs/AUTHORS.rst b/tests/wpt/tests/tools/third_party/attrs/AUTHORS.rst deleted file mode 100644 index f14ef6c6074..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/AUTHORS.rst +++ /dev/null @@ -1,11 +0,0 @@ -Credits -======= - -``attrs`` is written and maintained by `Hynek Schlawack <https://hynek.me/>`_. - -The development is kindly supported by `Variomedia AG <https://www.variomedia.de/>`_. - -A full list of contributors can be found in `GitHub's overview <https://github.com/python-attrs/attrs/graphs/contributors>`_. - -It’s the spiritual successor of `characteristic <https://characteristic.readthedocs.io/>`_ and aspires to fix some of it clunkiness and unfortunate decisions. -Both were inspired by Twisted’s `FancyEqMixin <https://twistedmatrix.com/documents/current/api/twisted.python.util.FancyEqMixin.html>`_ but both are implemented using class decorators because `subclassing is bad for you <https://www.youtube.com/watch?v=3MNVP9-hglc>`_, m’kay? diff --git a/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.md b/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.md new file mode 100644 index 00000000000..a768197ae1d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.md @@ -0,0 +1,1128 @@ +# Changelog + +Versions follow [CalVer](https://calver.org) with a strict backwards-compatibility policy. + +The **first number** of the version is the year. +The **second number** is incremented with each release, starting at 1 for each year. +The **third number** is when we need to start branches for older releases (only for emergencies). + +You can find out backwards-compatibility policy [here](https://github.com/python-attrs/attrs/blob/main/.github/SECURITY.md). + +Changes for the upcoming release can be found in the ["changelog.d" directory](https://github.com/python-attrs/attrs/tree/main/changelog.d) in our repository. + +<!-- towncrier release notes start --> + +## [23.2.0](https://github.com/python-attrs/attrs/tree/23.2.0) - 2023-12-31 + +### Changes + +- The type annotation for `attrs.resolve_types()` is now correct. + [#1141](https://github.com/python-attrs/attrs/issues/1141) +- Type stubs now use `typing.dataclass_transform` to decorate dataclass-like decorators, instead of the non-standard `__dataclass_transform__` special form, which is only supported by Pyright. + [#1158](https://github.com/python-attrs/attrs/issues/1158) +- Fixed serialization of namedtuple fields using `attrs.asdict/astuple()` with `retain_collection_types=True`. + [#1165](https://github.com/python-attrs/attrs/issues/1165) +- `attrs.AttrsInstance` is now a `typing.Protocol` in both type hints and code. + This allows you to subclass it along with another `Protocol`. + [#1172](https://github.com/python-attrs/attrs/issues/1172) +- If *attrs* detects that `__attrs_pre_init__` accepts more than just `self`, it will call it with the same arguments as `__init__` was called. + This allows you to, for example, pass arguments to `super().__init__()`. + [#1187](https://github.com/python-attrs/attrs/issues/1187) +- Slotted classes now transform `functools.cached_property` decorated methods to support equivalent semantics. + [#1200](https://github.com/python-attrs/attrs/issues/1200) +- Added *class_body* argument to `attrs.make_class()` to provide additional attributes for newly created classes. + It is, for example, now possible to attach methods. + [#1203](https://github.com/python-attrs/attrs/issues/1203) + +## [23.1.0](https://github.com/python-attrs/attrs/tree/23.1.0) - 2023-04-16 + +### Backwards-incompatible Changes + +- Python 3.6 has been dropped and packaging switched to static package data using [Hatch](https://hatch.pypa.io/latest/). + [#993](https://github.com/python-attrs/attrs/issues/993) + + +### Deprecations + +- The support for *zope-interface* via the `attrs.validators.provides` validator is now deprecated and will be removed in, or after, April 2024. + + The presence of a C-based package in our developement dependencies has caused headaches and we're not under the impression it's used a lot. + + Let us know if you're using it and we might publish it as a separate package. + [#1120](https://github.com/python-attrs/attrs/issues/1120) + + +### Changes + +- `attrs.filters.exclude()` and `attrs.filters.include()` now support the passing of attribute names as strings. + [#1068](https://github.com/python-attrs/attrs/issues/1068) +- `attrs.has()` and `attrs.fields()` now handle generic classes correctly. + [#1079](https://github.com/python-attrs/attrs/issues/1079) +- Fix frozen exception classes when raised within e.g. `contextlib.contextmanager`, which mutates their `__traceback__` attributes. + [#1081](https://github.com/python-attrs/attrs/issues/1081) +- `@frozen` now works with type checkers that implement [PEP-681](https://peps.python.org/pep-0681/) (ex. [pyright](https://github.com/microsoft/pyright/)). + [#1084](https://github.com/python-attrs/attrs/issues/1084) +- Restored ability to unpickle instances pickled before 22.2.0. + [#1085](https://github.com/python-attrs/attrs/issues/1085) +- `attrs.asdict()`'s and `attrs.astuple()`'s type stubs now accept the `attrs.AttrsInstance` protocol. + [#1090](https://github.com/python-attrs/attrs/issues/1090) +- Fix slots class cellvar updating closure in CPython 3.8+ even when `__code__` introspection is unavailable. + [#1092](https://github.com/python-attrs/attrs/issues/1092) +- `attrs.resolve_types()` can now pass `include_extras` to `typing.get_type_hints()` on Python 3.9+, and does so by default. + [#1099](https://github.com/python-attrs/attrs/issues/1099) +- Added instructions for pull request workflow to `CONTRIBUTING.md`. + [#1105](https://github.com/python-attrs/attrs/issues/1105) +- Added *type* parameter to `attrs.field()` function for use with `attrs.make_class()`. + + Please note that type checkers ignore type metadata passed into `make_class()`, but it can be useful if you're wrapping _attrs_. + [#1107](https://github.com/python-attrs/attrs/issues/1107) +- It is now possible for `attrs.evolve()` (and `attr.evolve()`) to change fields named `inst` if the instance is passed as a positional argument. + + Passing the instance using the `inst` keyword argument is now deprecated and will be removed in, or after, April 2024. + [#1117](https://github.com/python-attrs/attrs/issues/1117) +- `attrs.validators.optional()` now also accepts a tuple of validators (in addition to lists of validators). + [#1122](https://github.com/python-attrs/attrs/issues/1122) + + +## [22.2.0](https://github.com/python-attrs/attrs/tree/22.2.0) - 2022-12-21 + +### Backwards-incompatible Changes + +- Python 3.5 is not supported anymore. + [#988](https://github.com/python-attrs/attrs/issues/988) + + +### Deprecations + +- Python 3.6 is now deprecated and support will be removed in the next release. + [#1017](https://github.com/python-attrs/attrs/issues/1017) + + +### Changes + +- `attrs.field()` now supports an *alias* option for explicit `__init__` argument names. + + Get `__init__` signatures matching any taste, peculiar or plain! + The [PEP 681 compatible](https://peps.python.org/pep-0681/#field-specifier-parameters) *alias* option can be use to override private attribute name mangling, or add other arbitrary field argument name overrides. + [#950](https://github.com/python-attrs/attrs/issues/950) +- `attrs.NOTHING` is now an enum value, making it possible to use with e.g. [`typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal). + [#983](https://github.com/python-attrs/attrs/issues/983) +- Added missing re-import of `attr.AttrsInstance` to the `attrs` namespace. + [#987](https://github.com/python-attrs/attrs/issues/987) +- Fix slight performance regression in classes with custom `__setattr__` and speedup even more. + [#991](https://github.com/python-attrs/attrs/issues/991) +- Class-creation performance improvements by switching performance-sensitive templating operations to f-strings. + + You can expect an improvement of about 5% -- even for very simple classes. + [#995](https://github.com/python-attrs/attrs/issues/995) +- `attrs.has()` is now a [`TypeGuard`](https://docs.python.org/3/library/typing.html#typing.TypeGuard) for `AttrsInstance`. + That means that type checkers know a class is an instance of an `attrs` class if you check it using `attrs.has()` (or `attr.has()`) first. + [#997](https://github.com/python-attrs/attrs/issues/997) +- Made `attrs.AttrsInstance` stub available at runtime and fixed type errors related to the usage of `attrs.AttrsInstance` in *Pyright*. + [#999](https://github.com/python-attrs/attrs/issues/999) +- On Python 3.10 and later, call [`abc.update_abstractmethods()`](https://docs.python.org/3/library/abc.html#abc.update_abstractmethods) on dict classes after creation. + This improves the detection of abstractness. + [#1001](https://github.com/python-attrs/attrs/issues/1001) +- *attrs*'s pickling methods now use dicts instead of tuples. + That is safer and more robust across different versions of a class. + [#1009](https://github.com/python-attrs/attrs/issues/1009) +- Added `attrs.validators.not_(wrapped_validator)` to logically invert *wrapped_validator* by accepting only values where *wrapped_validator* rejects the value with a `ValueError` or `TypeError` (by default, exception types configurable). + [#1010](https://github.com/python-attrs/attrs/issues/1010) +- The type stubs for `attrs.cmp_using()` now have default values. + [#1027](https://github.com/python-attrs/attrs/issues/1027) +- To conform with [PEP 681](https://peps.python.org/pep-0681/), `attr.s()` and `attrs.define()` now accept *unsafe_hash* in addition to *hash*. + [#1065](https://github.com/python-attrs/attrs/issues/1065) + + +## [22.1.0](https://github.com/python-attrs/attrs/tree/22.1.0) - 2022-07-28 + +### Backwards-incompatible Changes + +- Python 2.7 is not supported anymore. + + Dealing with Python 2.7 tooling has become too difficult for a volunteer-run project. + + We have supported Python 2 more than 2 years after it was officially discontinued and feel that we have paid our dues. + All version up to 21.4.0 from December 2021 remain fully functional, of course. + [#936](https://github.com/python-attrs/attrs/issues/936) + +- The deprecated `cmp` attribute of `attrs.Attribute` has been removed. + This does not affect the *cmp* argument to `attr.s` that can be used as a shortcut to set *eq* and *order* at the same time. + [#939](https://github.com/python-attrs/attrs/issues/939) + + +### Changes + +- Instantiation of frozen slotted classes is now faster. + [#898](https://github.com/python-attrs/attrs/issues/898) +- If an `eq` key is defined, it is also used before hashing the attribute. + [#909](https://github.com/python-attrs/attrs/issues/909) +- Added `attrs.validators.min_len()`. + [#916](https://github.com/python-attrs/attrs/issues/916) +- `attrs.validators.deep_iterable()`'s *member_validator* argument now also accepts a list of validators and wraps them in an `attrs.validators.and_()`. + [#925](https://github.com/python-attrs/attrs/issues/925) +- Added missing type stub re-imports for `attrs.converters` and `attrs.filters`. + [#931](https://github.com/python-attrs/attrs/issues/931) +- Added missing stub for `attr(s).cmp_using()`. + [#949](https://github.com/python-attrs/attrs/issues/949) +- `attrs.validators._in()`'s `ValueError` is not missing the attribute, expected options, and the value it got anymore. + [#951](https://github.com/python-attrs/attrs/issues/951) +- Python 3.11 is now officially supported. + [#969](https://github.com/python-attrs/attrs/issues/969) + + +## [21.4.0](https://github.com/python-attrs/attrs/tree/21.4.0) - 2021-12-29 + +### Changes + +- Fixed the test suite on PyPy3.8 where `cloudpickle` does not work. + [#892](https://github.com/python-attrs/attrs/issues/892) +- Fixed `coverage report` for projects that use `attrs` and don't set a `--source`. + [#895](https://github.com/python-attrs/attrs/issues/895), + [#896](https://github.com/python-attrs/attrs/issues/896) + + +## [21.3.0](https://github.com/python-attrs/attrs/tree/21.3.0) - 2021-12-28 + +### Backward-incompatible Changes + +- When using `@define`, converters are now run by default when setting an attribute on an instance -- additionally to validators. + I.e. the new default is `on_setattr=[attrs.setters.convert, attrs.setters.validate]`. + + This is unfortunately a breaking change, but it was an oversight, impossible to raise a `DeprecationWarning` about, and it's better to fix it now while the APIs are very fresh with few users. + [#835](https://github.com/python-attrs/attrs/issues/835), + [#886](https://github.com/python-attrs/attrs/issues/886) + +- `import attrs` has finally landed! + As of this release, you can finally import `attrs` using its proper name. + + Not all names from the `attr` namespace have been transferred; most notably `attr.s` and `attr.ib` are missing. + See `attrs.define` and `attrs.field` if you haven't seen our next-generation APIs yet. + A more elaborate explanation can be found [On The Core API Names](https://www.attrs.org/en/latest/names.html) + + This feature is at least for one release **provisional**. + We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. + + The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. + Please note that we have **not** moved -- or even removed -- anything from `attr`! + + Please do report any bugs or documentation inconsistencies! + [#887](https://github.com/python-attrs/attrs/issues/887) + + +### Changes + +- `attr.asdict(retain_collection_types=False)` (default) dumps collection-esque keys as tuples. + [#646](https://github.com/python-attrs/attrs/issues/646), + [#888](https://github.com/python-attrs/attrs/issues/888) +- `__match_args__` are now generated to support Python 3.10's + [Structural Pattern Matching](https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching). + This can be controlled by the `match_args` argument to the class decorators on Python 3.10 and later. + On older versions, it is never added and the argument is ignored. + [#815](https://github.com/python-attrs/attrs/issues/815) +- If the class-level *on_setattr* is set to `attrs.setters.validate` (default in `@define` and `@mutable`) but no field defines a validator, pretend that it's not set. + [#817](https://github.com/python-attrs/attrs/issues/817) +- The generated `__repr__` is significantly faster on Pythons with f-strings. + [#819](https://github.com/python-attrs/attrs/issues/819) +- Attributes transformed via `field_transformer` are wrapped with `AttrsClass` again. + [#824](https://github.com/python-attrs/attrs/issues/824) +- Generated source code is now cached more efficiently for identical classes. + [#828](https://github.com/python-attrs/attrs/issues/828) +- Added `attrs.converters.to_bool()`. + [#830](https://github.com/python-attrs/attrs/issues/830) +- `attrs.resolve_types()` now resolves types of subclasses after the parents are resolved. + [#842](https://github.com/python-attrs/attrs/issues/842) + [#843](https://github.com/python-attrs/attrs/issues/843) +- Added new validators: `lt(val)` (\< val), `le(va)` (≤ val), `ge(val)` (≥ val), `gt(val)` (> val), and `maxlen(n)`. + [#845](https://github.com/python-attrs/attrs/issues/845) +- `attrs` classes are now fully compatible with [cloudpickle](https://github.com/cloudpipe/cloudpickle) (no need to disable `repr` anymore). + [#857](https://github.com/python-attrs/attrs/issues/857) +- Added new context manager `attrs.validators.disabled()` and functions `attrs.validators.(set|get)_disabled()`. + They deprecate `attrs.(set|get)_run_validators()`. + All functions are interoperable and modify the same internal state. + They are not – and never were – thread-safe, though. + [#859](https://github.com/python-attrs/attrs/issues/859) +- `attrs.validators.matches_re()` now accepts pre-compiled regular expressions in addition to pattern strings. + [#877](https://github.com/python-attrs/attrs/issues/877) + +--- + +## [21.2.0](https://github.com/python-attrs/attrs/tree/21.2.0) - 2021-05-07 + +### Backward-incompatible Changes + +- We had to revert the recursive feature for `attr.evolve()` because it broke some use-cases -- sorry! + [#806](https://github.com/python-attrs/attrs/issues/806) +- Python 3.4 is now blocked using packaging metadata because `attrs` can't be imported on it anymore. + To ensure that 3.4 users can keep installing `attrs` easily, we will [yank](https://pypi.org/help/#yanked) 21.1.0 from PyPI. + This has **no** consequences if you pin `attrs` to 21.1.0. + [#807](https://github.com/python-attrs/attrs/issues/807) + + +## [21.1.0](https://github.com/python-attrs/attrs/tree/21.1.0) - 2021-05-06 + +### Deprecations + +- The long-awaited, much-talked-about, little-delivered `import attrs` is finally upon us! + + Since the NG APIs have now been proclaimed stable, the **next** release of `attrs` will allow you to actually `import attrs`. + We're taking this opportunity to replace some defaults in our APIs that made sense in 2015, but don't in 2021. + + So please, if you have any pet peeves about defaults in `attrs`'s APIs, *now* is the time to air your grievances in #487! + We're not gonna get such a chance for a second time, without breaking our backward-compatibility guarantees, or long deprecation cycles. + Therefore, speak now or forever hold you peace! + [#487](https://github.com/python-attrs/attrs/issues/487) + +- The *cmp* argument to `attr.s()` and `attr.ib()` has been **undeprecated** + It will continue to be supported as syntactic sugar to set *eq* and *order* in one go. + + I'm terribly sorry for the hassle around this argument! + The reason we're bringing it back is it's usefulness regarding customization of equality/ordering. + + The `cmp` attribute and argument on `attr.Attribute` remains deprecated and will be removed later this year. + [#773](https://github.com/python-attrs/attrs/issues/773) + + +### Changes + +- It's now possible to customize the behavior of `eq` and `order` by passing in a callable. + [#435](https://github.com/python-attrs/attrs/issues/435), + [#627](https://github.com/python-attrs/attrs/issues/627) + +- The instant favorite next-generation APIs are not provisional anymore! + + They are also officially supported by Mypy as of their [0.800 release](https://mypy-lang.blogspot.com/2021/01/mypy-0800-released.html). + + We hope the next release will already contain an (additional) importable package called `attrs`. + [#668](https://github.com/python-attrs/attrs/issues/668), + [#786](https://github.com/python-attrs/attrs/issues/786) + +- If an attribute defines a converter, the type of its parameter is used as type annotation for its corresponding `__init__` parameter. + + If an `attr.converters.pipe` is used, the first one's is used. + [#710](https://github.com/python-attrs/attrs/issues/710) + +- Fixed the creation of an extra slot for an `attr.ib` when the parent class already has a slot with the same name. + [#718](https://github.com/python-attrs/attrs/issues/718) + +- `__attrs__init__()` will now be injected if `init=False`, or if `auto_detect=True` and a user-defined `__init__()` exists. + + This enables users to do "pre-init" work in their `__init__()` (such as `super().__init__()`). + + `__init__()` can then delegate constructor argument processing to `self.__attrs_init__(*args, **kwargs)`. + [#731](https://github.com/python-attrs/attrs/issues/731) + +- `bool(attr.NOTHING)` is now `False`. + [#732](https://github.com/python-attrs/attrs/issues/732) + +- It's now possible to use `super()` inside of properties of slotted classes. + [#747](https://github.com/python-attrs/attrs/issues/747) + +- Allow for a `__attrs_pre_init__()` method that -- if defined -- will get called at the beginning of the `attrs`-generated `__init__()` method. + [#750](https://github.com/python-attrs/attrs/issues/750) + +- Added forgotten `attr.Attribute.evolve()` to type stubs. + [#752](https://github.com/python-attrs/attrs/issues/752) + +- `attrs.evolve()` now works recursively with nested `attrs` classes. + [#759](https://github.com/python-attrs/attrs/issues/759) + +- Python 3.10 is now officially supported. + [#763](https://github.com/python-attrs/attrs/issues/763) + +- `attr.resolve_types()` now takes an optional *attrib* argument to work inside a `field_transformer`. + [#774](https://github.com/python-attrs/attrs/issues/774) + +- `ClassVar`s are now also detected if they come from [typing-extensions](https://pypi.org/project/typing-extensions/). + [#782](https://github.com/python-attrs/attrs/issues/782) + +- To make it easier to customize attribute comparison (#435), we have added the `attr.cmp_with()` helper. + + See the [new docs on comparison](https://www.attrs.org/en/stable/comparison.html) for more details. + [#787](https://github.com/python-attrs/attrs/issues/787) + +- Added **provisional** support for static typing in `pyright` via [PEP 681](https://peps.python.org/pep-0681/). + Both the `pyright` specification and `attrs` implementation may change in future versions of both projects. + + Your constructive feedback is welcome in both [attrs#795](https://github.com/python-attrs/attrs/issues/795) and [pyright#1782](https://github.com/microsoft/pyright/discussions/1782). + [#796](https://github.com/python-attrs/attrs/issues/796) + + +## [20.3.0](https://github.com/python-attrs/attrs/tree/20.3.0) - 2020-11-05 + +### Backward-incompatible Changes + +- `attr.define()`, `attr.frozen()`, `attr.mutable()`, and `attr.field()` remain **provisional**. + + This release does **not** change anything about them and they are already used widely in production though. + + If you wish to use them together with mypy, you can simply drop [this plugin](https://gist.github.com/hynek/1e3844d0c99e479e716169034b5fa963#file-attrs_ng_plugin-py) into your project. + + Feel free to provide feedback to them in the linked issue #668. + + We will release the `attrs` namespace once we have the feeling that the APIs have properly settled. + [#668](https://github.com/python-attrs/attrs/issues/668) + +### Changes + +- `attr.s()` now has a *field_transformer* hook that is called for all `Attribute`s and returns a (modified or updated) list of `Attribute` instances. + `attr.asdict()` has a *value_serializer* hook that can change the way values are converted. + Both hooks are meant to help with data (de-)serialization workflows. + [#653](https://github.com/python-attrs/attrs/issues/653) +- `kw_only=True` now works on Python 2. + [#700](https://github.com/python-attrs/attrs/issues/700) +- `raise from` now works on frozen classes on PyPy. + [#703](https://github.com/python-attrs/attrs/issues/703), + [#712](https://github.com/python-attrs/attrs/issues/712) +- `attr.asdict()` and `attr.astuple()` now treat `frozenset`s like `set`s with regards to the *retain_collection_types* argument. + [#704](https://github.com/python-attrs/attrs/issues/704) +- The type stubs for `attr.s()` and `attr.make_class()` are not missing the *collect_by_mro* argument anymore. + [#711](https://github.com/python-attrs/attrs/issues/711) + +--- + +## [20.2.0](https://github.com/python-attrs/attrs/tree/20.2.0) - 2020-09-05 + +### Backward-incompatible Changes + +- `attr.define()`, `attr.frozen()`, `attr.mutable()`, and `attr.field()` remain **provisional**. + + This release fixes a bunch of bugs and ergonomics but they remain mostly unchanged. + + If you wish to use them together with mypy, you can simply drop [this plugin](https://gist.github.com/hynek/1e3844d0c99e479e716169034b5fa963#file-attrs_ng_plugin-py) into your project. + + Feel free to provide feedback to them in the linked issue #668. + + We will release the `attrs` namespace once we have the feeling that the APIs have properly settled. + [#668](https://github.com/python-attrs/attrs/issues/668) + +### Changes + +- `attr.define()` et al now correctly detect `__eq__` and `__ne__`. + [#671](https://github.com/python-attrs/attrs/issues/671) + +- `attr.define()` et al's hybrid behavior now also works correctly when arguments are passed. + [#675](https://github.com/python-attrs/attrs/issues/675) + +- It's possible to define custom `__setattr__` methods on slotted classes again. + [#681](https://github.com/python-attrs/attrs/issues/681) + +- In 20.1.0 we introduced the `inherited` attribute on the `attr.Attribute` class to differentiate attributes that have been inherited and those that have been defined directly on the class. + + It has shown to be problematic to involve that attribute when comparing instances of `attr.Attribute` though, because when sub-classing, attributes from base classes are suddenly not equal to themselves in a super class. + + Therefore the `inherited` attribute will now be ignored when hashing and comparing instances of `attr.Attribute`. + [#684](https://github.com/python-attrs/attrs/issues/684) + +- `zope.interface` is now a "soft dependency" when running the test suite; if `zope.interface` is not installed when running the test suite, the interface-related tests will be automatically skipped. + [#685](https://github.com/python-attrs/attrs/issues/685) + +- The ergonomics of creating frozen classes using `@define(frozen=True)` and sub-classing frozen classes has been improved: + you don't have to set `on_setattr=None` anymore. + [#687](https://github.com/python-attrs/attrs/issues/687) + +--- + +## [20.1.0](https://github.com/python-attrs/attrs/tree/20.1.0) - 2020-08-20 + +### Backward-incompatible Changes + +- Python 3.4 is not supported anymore. + It has been unsupported by the Python core team for a while now, its PyPI downloads are negligible, and our CI provider removed it as a supported option. + + It's very unlikely that `attrs` will break under 3.4 anytime soon, which is why we do *not* block its installation on Python 3.4. + But we don't test it anymore and will block it once someone reports breakage. + [#608](https://github.com/python-attrs/attrs/issues/608) + +### Deprecations + +- Less of a deprecation and more of a heads up: the next release of `attrs` will introduce an `attrs` namespace. + That means that you'll finally be able to run `import attrs` with new functions that aren't cute abbreviations and that will carry better defaults. + + This should not break any of your code, because project-local packages have priority before installed ones. + If this is a problem for you for some reason, please report it to our bug tracker and we'll figure something out. + + The old `attr` namespace isn't going anywhere and its defaults are not changing – this is a purely additive measure. + Please check out the linked issue for more details. + + These new APIs have been added *provisionally* as part of #666 so you can try them out today and provide feedback. + Learn more in the [API docs](https://www.attrs.org/en/stable/api.html). + [#408](https://github.com/python-attrs/attrs/issues/408) + +### Changes + +- Added `attr.resolve_types()`. + It ensures that all forward-references and types in string form are resolved into concrete types. + + You need this only if you need concrete types at runtime. + That means that if you only use types for static type checking, you do **not** need this function. + [#288](https://github.com/python-attrs/attrs/issues/288), + [#302](https://github.com/python-attrs/attrs/issues/302) + +- Added `@attr.s(collect_by_mro=False)` argument that if set to `True` fixes the collection of attributes from base classes. + + It's only necessary for certain cases of multiple-inheritance but is kept off for now for backward-compatibility reasons. + It will be turned on by default in the future. + + As a side-effect, `attr.Attribute` now *always* has an `inherited` attribute indicating whether an attribute on a class was directly defined or inherited. + [#428](https://github.com/python-attrs/attrs/issues/428), + [#635](https://github.com/python-attrs/attrs/issues/635) + +- On Python 3, all generated methods now have a docstring explaining that they have been created by `attrs`. + [#506](https://github.com/python-attrs/attrs/issues/506) + +- It is now possible to prevent `attrs` from auto-generating the `__setstate__` and `__getstate__` methods that are required for pickling of slotted classes. + + Either pass `@attr.s(getstate_setstate=False)` or pass `@attr.s(auto_detect=True)` and implement them yourself: + if `attrs` finds either of the two methods directly on the decorated class, it assumes implicitly `getstate_setstate=False` (and implements neither). + + This option works with dict classes but should never be necessary. + [#512](https://github.com/python-attrs/attrs/issues/512), + [#513](https://github.com/python-attrs/attrs/issues/513), + [#642](https://github.com/python-attrs/attrs/issues/642) + +- Fixed a `ValueError: Cell is empty` bug that could happen in some rare edge cases. + [#590](https://github.com/python-attrs/attrs/issues/590) + +- `attrs` can now automatically detect your own implementations and infer `init=False`, `repr=False`, `eq=False`, `order=False`, and `hash=False` if you set `@attr.s(auto_detect=True)`. + `attrs` will ignore inherited methods. + If the argument implies more than one method (e.g. `eq=True` creates both `__eq__` and `__ne__`), it's enough for *one* of them to exist and `attrs` will create *neither*. + + This feature requires Python 3. + [#607](https://github.com/python-attrs/attrs/issues/607) + +- Added `attr.converters.pipe()`. + The feature allows combining multiple conversion callbacks into one by piping the value through all of them, and retuning the last result. + + As part of this feature, we had to relax the type information for converter callables. + [#618](https://github.com/python-attrs/attrs/issues/618) + +- Fixed serialization behavior of non-slots classes with `cache_hash=True`. + The hash cache will be cleared on operations which make "deep copies" of instances of classes with hash caching, + though the cache will not be cleared with shallow copies like those made by `copy.copy()`. + + Previously, `copy.deepcopy()` or serialization and deserialization with `pickle` would result in an un-initialized object. + + This change also allows the creation of `cache_hash=True` classes with a custom `__setstate__`, + which was previously forbidden ([#494](https://github.com/python-attrs/attrs/issues/494)). + [#620](https://github.com/python-attrs/attrs/issues/620) + +- It is now possible to specify hooks that are called whenever an attribute is set **after** a class has been instantiated. + + You can pass `on_setattr` both to `@attr.s()` to set the default for all attributes on a class, and to `@attr.ib()` to overwrite it for individual attributes. + + `attrs` also comes with a new module `attr.setters` that brings helpers that run validators, converters, or allow to freeze a subset of attributes. + [#645](https://github.com/python-attrs/attrs/issues/645), + [#660](https://github.com/python-attrs/attrs/issues/660) + +- **Provisional** APIs called `attr.define()`, `attr.mutable()`, and `attr.frozen()` have been added. + + They are only available on Python 3.6 and later, and call `attr.s()` with different default values. + + If nothing comes up, they will become the official way for creating classes in 20.2.0 (see above). + + **Please note** that it may take some time until mypy – and other tools that have dedicated support for `attrs` – recognize these new APIs. + Please **do not** open issues on our bug tracker, there is nothing we can do about it. + [#666](https://github.com/python-attrs/attrs/issues/666) + +- We have also provisionally added `attr.field()` that supplants `attr.ib()`. + It also requires at least Python 3.6 and is keyword-only. + Other than that, it only dropped a few arguments, but changed no defaults. + + As with `attr.s()`: `attr.ib()` is not going anywhere. + [#669](https://github.com/python-attrs/attrs/issues/669) + +--- + +## [19.3.0](https://github.com/python-attrs/attrs/tree/19.3.0) - 2019-10-15 + +### Changes + +- Fixed `auto_attribs` usage when default values cannot be compared directly with `==`, such as `numpy` arrays. + [#585](https://github.com/python-attrs/attrs/issues/585) + +--- + +## [19.2.0](https://github.com/python-attrs/attrs/tree/19.2.0) - 2019-10-01 + +### Backward-incompatible Changes + +- Removed deprecated `Attribute` attribute `convert` per scheduled removal on 2019/1. + This planned deprecation is tracked in issue [#307](https://github.com/python-attrs/attrs/issues/307). + [#504](https://github.com/python-attrs/attrs/issues/504) + +- `__lt__`, `__le__`, `__gt__`, and `__ge__` do not consider subclasses comparable anymore. + + This has been deprecated since 18.2.0 and was raising a `DeprecationWarning` for over a year. + [#570](https://github.com/python-attrs/attrs/issues/570) + +### Deprecations + +- The `cmp` argument to `attr.s()` and `attr.ib()` is now deprecated. + + Please use `eq` to add equality methods (`__eq__` and `__ne__`) and `order` to add ordering methods (`__lt__`, `__le__`, `__gt__`, and `__ge__`) instead – just like with [dataclasses](https://docs.python.org/3/library/dataclasses.html). + + Both are effectively `True` by default but it's enough to set `eq=False` to disable both at once. + Passing `eq=False, order=True` explicitly will raise a `ValueError` though. + + Since this is arguably a deeper backward-compatibility break, it will have an extended deprecation period until 2021-06-01. + After that day, the `cmp` argument will be removed. + + `attr.Attribute` also isn't orderable anymore. + [#574](https://github.com/python-attrs/attrs/issues/574) + +### Changes + +- Updated `attr.validators.__all__` to include new validators added in [#425]. + [#517](https://github.com/python-attrs/attrs/issues/517) +- Slotted classes now use a pure Python mechanism to rewrite the `__class__` cell when rebuilding the class, so `super()` works even on environments where `ctypes` is not installed. + [#522](https://github.com/python-attrs/attrs/issues/522) +- When collecting attributes using `@attr.s(auto_attribs=True)`, attributes with a default of `None` are now deleted too. + [#523](https://github.com/python-attrs/attrs/issues/523), + [#556](https://github.com/python-attrs/attrs/issues/556) +- Fixed `attr.validators.deep_iterable()` and `attr.validators.deep_mapping()` type stubs. + [#533](https://github.com/python-attrs/attrs/issues/533) +- `attr.validators.is_callable()` validator now raises an exception `attr.exceptions.NotCallableError`, a subclass of `TypeError`, informing the received value. + [#536](https://github.com/python-attrs/attrs/issues/536) +- `@attr.s(auto_exc=True)` now generates classes that are hashable by ID, as the documentation always claimed it would. + [#543](https://github.com/python-attrs/attrs/issues/543), + [#563](https://github.com/python-attrs/attrs/issues/563) +- Added `attr.validators.matches_re()` that checks string attributes whether they match a regular expression. + [#552](https://github.com/python-attrs/attrs/issues/552) +- Keyword-only attributes (`kw_only=True`) and attributes that are excluded from the `attrs`'s `__init__` (`init=False`) now can appear before mandatory attributes. + [#559](https://github.com/python-attrs/attrs/issues/559) +- The fake filename for generated methods is now more stable. + It won't change when you restart the process. + [#560](https://github.com/python-attrs/attrs/issues/560) +- The value passed to `@attr.ib(repr=…)` can now be either a boolean (as before) or a callable. + That callable must return a string and is then used for formatting the attribute by the generated `__repr__()` method. + [#568](https://github.com/python-attrs/attrs/issues/568) +- Added `attr.__version_info__` that can be used to reliably check the version of `attrs` and write forward- and backward-compatible code. + Please check out the [section on deprecated APIs](https://www.attrs.org/en/stable/api-attr.html#deprecated-apis) on how to use it. + [#580](https://github.com/python-attrs/attrs/issues/580) + +> + +--- + +## [19.1.0](https://github.com/python-attrs/attrs/tree/19.1.0) - 2019-03-03 + +### Backward-incompatible Changes + +- Fixed a bug where deserialized objects with `cache_hash=True` could have incorrect hash code values. + This change breaks classes with `cache_hash=True` when a custom `__setstate__` is present. + An exception will be thrown when applying the `attrs` annotation to such a class. + This limitation is tracked in issue [#494](https://github.com/python-attrs/attrs/issues/494). + [#482](https://github.com/python-attrs/attrs/issues/482) + +### Changes + +- Add `is_callable`, `deep_iterable`, and `deep_mapping` validators. + + - `is_callable`: validates that a value is callable + - `deep_iterable`: Allows recursion down into an iterable, + applying another validator to every member in the iterable + as well as applying an optional validator to the iterable itself. + - `deep_mapping`: Allows recursion down into the items in a mapping object, + applying a key validator and a value validator to the key and value in every item. + Also applies an optional validator to the mapping object itself. + + You can find them in the `attr.validators` package. + [#425] + +- Fixed stub files to prevent errors raised by mypy's `disallow_any_generics = True` option. + [#443](https://github.com/python-attrs/attrs/issues/443) + +- Attributes with `init=False` now can follow after `kw_only=True` attributes. + [#450](https://github.com/python-attrs/attrs/issues/450) + +- `attrs` now has first class support for defining exception classes. + + If you define a class using `@attr.s(auto_exc=True)` and subclass an exception, the class will behave like a well-behaved exception class including an appropriate `__str__` method, and all attributes additionally available in an `args` attribute. + [#500](https://github.com/python-attrs/attrs/issues/500) + +- Clarified documentation for hashing to warn that hashable objects should be deeply immutable (in their usage, even if this is not enforced). + [#503](https://github.com/python-attrs/attrs/issues/503) + +--- + +## [18.2.0](https://github.com/python-attrs/attrs/tree/18.2.0) - 2018-09-01 + +### Deprecations + +- Comparing subclasses using `<`, `>`, `<=`, and `>=` is now deprecated. + The docs always claimed that instances are only compared if the types are identical, so this is a first step to conform to the docs. + + Equality operators (`==` and `!=`) were always strict in this regard. + [#394](https://github.com/python-attrs/attrs/issues/394) + +### Changes + +- `attrs` now ships its own [PEP 484](https://peps.python.org/pep-0484/) type hints. + Together with [mypy](http://mypy-lang.org)'s `attrs` plugin, you've got all you need for writing statically typed code in both Python 2 and 3! + + At that occasion, we've also added [narrative docs](https://www.attrs.org/en/stable/types.html) about type annotations in `attrs`. + [#238](https://github.com/python-attrs/attrs/issues/238) + +- Added *kw_only* arguments to `attr.ib` and `attr.s`, and a corresponding *kw_only* attribute to `attr.Attribute`. + This change makes it possible to have a generated `__init__` with keyword-only arguments on Python 3, relaxing the required ordering of default and non-default valued attributes. + [#281](https://github.com/python-attrs/attrs/issues/281), + [#411](https://github.com/python-attrs/attrs/issues/411) + +- The test suite now runs with `hypothesis.HealthCheck.too_slow` disabled to prevent CI breakage on slower computers. + [#364](https://github.com/python-attrs/attrs/issues/364), + [#396](https://github.com/python-attrs/attrs/issues/396) + +- `attr.validators.in_()` now raises a `ValueError` with a useful message even if the options are a string and the value is not a string. + [#383](https://github.com/python-attrs/attrs/issues/383) + +- `attr.asdict()` now properly handles deeply nested lists and dictionaries. + [#395](https://github.com/python-attrs/attrs/issues/395) + +- Added `attr.converters.default_if_none()` that allows to replace `None` values in attributes. + For example `attr.ib(converter=default_if_none(""))` replaces `None` by empty strings. + [#400](https://github.com/python-attrs/attrs/issues/400), + [#414](https://github.com/python-attrs/attrs/issues/414) + +- Fixed a reference leak where the original class would remain live after being replaced when `slots=True` is set. + [#407](https://github.com/python-attrs/attrs/issues/407) + +- Slotted classes can now be made weakly referenceable by passing `@attr.s(weakref_slot=True)`. + [#420](https://github.com/python-attrs/attrs/issues/420) + +- Added *cache_hash* option to `@attr.s` which causes the hash code to be computed once and stored on the object. + [#426](https://github.com/python-attrs/attrs/issues/426) + +- Attributes can be named `property` and `itemgetter` now. + [#430](https://github.com/python-attrs/attrs/issues/430) + +- It is now possible to override a base class' class variable using only class annotations. + [#431](https://github.com/python-attrs/attrs/issues/431) + +--- + +## [18.1.0](https://github.com/python-attrs/attrs/tree/18.1.0) - 2018-05-03 + +### Changes + +- `x=X(); x.cycle = x; repr(x)` will no longer raise a `RecursionError`, and will instead show as `X(x=...)`. + + [#95](https://github.com/python-attrs/attrs/issues/95) + +- `attr.ib(factory=f)` is now syntactic sugar for the common case of `attr.ib(default=attr.Factory(f))`. + + [#178](https://github.com/python-attrs/attrs/issues/178), + [#356](https://github.com/python-attrs/attrs/issues/356) + +- Added `attr.field_dict()` to return an ordered dictionary of `attrs` attributes for a class, whose keys are the attribute names. + + [#290](https://github.com/python-attrs/attrs/issues/290), + [#349](https://github.com/python-attrs/attrs/issues/349) + +- The order of attributes that are passed into `attr.make_class()` or the *these* argument of `@attr.s()` is now retained if the dictionary is ordered (i.e. `dict` on Python 3.6 and later, `collections.OrderedDict` otherwise). + + Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programmatically. + + [#300](https://github.com/python-attrs/attrs/issues/300), + [#339](https://github.com/python-attrs/attrs/issues/339), + [#343](https://github.com/python-attrs/attrs/issues/343) + +- In slotted classes, `__getstate__` and `__setstate__` now ignore the `__weakref__` attribute. + + [#311](https://github.com/python-attrs/attrs/issues/311), + [#326](https://github.com/python-attrs/attrs/issues/326) + +- Setting the cell type is now completely best effort. + This fixes `attrs` on Jython. + + We cannot make any guarantees regarding Jython though, because our test suite cannot run due to dependency incompatibilities. + + [#321](https://github.com/python-attrs/attrs/issues/321), + [#334](https://github.com/python-attrs/attrs/issues/334) + +- If `attr.s` is passed a *these* argument, it will no longer attempt to remove attributes with the same name from the class body. + + [#322](https://github.com/python-attrs/attrs/issues/322), + [#323](https://github.com/python-attrs/attrs/issues/323) + +- The hash of `attr.NOTHING` is now vegan and faster on 32bit Python builds. + + [#331](https://github.com/python-attrs/attrs/issues/331), + [#332](https://github.com/python-attrs/attrs/issues/332) + +- The overhead of instantiating frozen dict classes is virtually eliminated. + [#336](https://github.com/python-attrs/attrs/issues/336) + +- Generated `__init__` methods now have an `__annotations__` attribute derived from the types of the fields. + + [#363](https://github.com/python-attrs/attrs/issues/363) + +- We have restructured the documentation a bit to account for `attrs`' growth in scope. + Instead of putting everything into the [examples](https://www.attrs.org/en/stable/examples.html) page, we have started to extract narrative chapters. + + So far, we've added chapters on [initialization](https://www.attrs.org/en/stable/init.html) and [hashing](https://www.attrs.org/en/stable/hashing.html). + + Expect more to come! + + [#369](https://github.com/python-attrs/attrs/issues/369), + [#370](https://github.com/python-attrs/attrs/issues/370) + +--- + +## [17.4.0](https://github.com/python-attrs/attrs/tree/17.4.0) - 2017-12-30 + +### Backward-incompatible Changes + +- The traversal of MROs when using multiple inheritance was backward: + If you defined a class `C` that subclasses `A` and `B` like `C(A, B)`, `attrs` would have collected the attributes from `B` *before* those of `A`. + + This is now fixed and means that in classes that employ multiple inheritance, the output of `__repr__` and the order of positional arguments in `__init__` changes. + Because of the nature of this bug, a proper deprecation cycle was unfortunately impossible. + + Generally speaking, it's advisable to prefer `kwargs`-based initialization anyways – *especially* if you employ multiple inheritance and diamond-shaped hierarchies. + + [#298](https://github.com/python-attrs/attrs/issues/298), + [#299](https://github.com/python-attrs/attrs/issues/299), + [#304](https://github.com/python-attrs/attrs/issues/304) + +- The `__repr__` set by `attrs` no longer produces an `AttributeError` when the instance is missing some of the specified attributes (either through deleting or after using `init=False` on some attributes). + + This can break code that relied on `repr(attr_cls_instance)` raising `AttributeError` to check if any `attrs`-specified members were unset. + + If you were using this, you can implement a custom method for checking this: + + ``` + def has_unset_members(self): + for field in attr.fields(type(self)): + try: + getattr(self, field.name) + except AttributeError: + return True + return False + ``` + + [#308](https://github.com/python-attrs/attrs/issues/308) + +### Deprecations + +- The `attr.ib(convert=callable)` option is now deprecated in favor of `attr.ib(converter=callable)`. + + This is done to achieve consistency with other noun-based arguments like *validator*. + + *convert* will keep working until at least January 2019 while raising a `DeprecationWarning`. + + [#307](https://github.com/python-attrs/attrs/issues/307) + +### Changes + +- Generated `__hash__` methods now hash the class type along with the attribute values. + Until now the hashes of two classes with the same values were identical which was a bug. + + The generated method is also *much* faster now. + + [#261](https://github.com/python-attrs/attrs/issues/261), + [#295](https://github.com/python-attrs/attrs/issues/295), + [#296](https://github.com/python-attrs/attrs/issues/296) + +- `attr.ib`’s *metadata* argument now defaults to a unique empty `dict` instance instead of sharing a common empty `dict` for all. + The singleton empty `dict` is still enforced. + + [#280](https://github.com/python-attrs/attrs/issues/280) + +- `ctypes` is optional now however if it's missing, a bare `super()` will not work in slotted classes. + This should only happen in special environments like Google App Engine. + + [#284](https://github.com/python-attrs/attrs/issues/284), + [#286](https://github.com/python-attrs/attrs/issues/286) + +- The attribute redefinition feature introduced in 17.3.0 now takes into account if an attribute is redefined via multiple inheritance. + In that case, the definition that is closer to the base of the class hierarchy wins. + + [#285](https://github.com/python-attrs/attrs/issues/285), + [#287](https://github.com/python-attrs/attrs/issues/287) + +- Subclasses of `auto_attribs=True` can be empty now. + + [#291](https://github.com/python-attrs/attrs/issues/291), + [#292](https://github.com/python-attrs/attrs/issues/292) + +- Equality tests are *much* faster now. + + [#306](https://github.com/python-attrs/attrs/issues/306) + +- All generated methods now have correct `__module__`, `__name__`, and (on Python 3) `__qualname__` attributes. + + [#309](https://github.com/python-attrs/attrs/issues/309) + +--- + +## [17.3.0](https://github.com/python-attrs/attrs/tree/17.3.0) - 2017-11-08 + +### Backward-incompatible Changes + +- Attributes are no longer defined on the class body. + + This means that if you define a class `C` with an attribute `x`, the class will *not* have an attribute `x` for introspection. + Instead of `C.x`, use `attr.fields(C).x` or look at `C.__attrs_attrs__`. + The old behavior has been deprecated since version 16.1. + ([#253](https://github.com/python-attrs/attrs/issues/253)) + +### Changes + +- `super()` and `__class__` now work with slotted classes on Python 3. + ([#102](https://github.com/python-attrs/attrs/issues/102), [#226](https://github.com/python-attrs/attrs/issues/226), [#269](https://github.com/python-attrs/attrs/issues/269), [#270](https://github.com/python-attrs/attrs/issues/270), [#272](https://github.com/python-attrs/attrs/issues/272)) + +- Added *type* argument to `attr.ib()` and corresponding `type` attribute to `attr.Attribute`. + + This change paves the way for automatic type checking and serialization (though as of this release `attrs` does not make use of it). + In Python 3.6 or higher, the value of `attr.Attribute.type` can alternately be set using variable type annotations + (see [PEP 526](https://peps.python.org/pep-0526/)). + ([#151](https://github.com/python-attrs/attrs/issues/151), [#214](https://github.com/python-attrs/attrs/issues/214), [#215](https://github.com/python-attrs/attrs/issues/215), [#239](https://github.com/python-attrs/attrs/issues/239)) + +- The combination of `str=True` and `slots=True` now works on Python 2. + ([#198](https://github.com/python-attrs/attrs/issues/198)) + +- `attr.Factory` is hashable again. + ([#204](https://github.com/python-attrs/attrs/issues/204)) + +- Subclasses now can overwrite attribute definitions of their base classes. + + That means that you can -- for example -- change the default value for an attribute by redefining it. + ([#221](https://github.com/python-attrs/attrs/issues/221), [#229](https://github.com/python-attrs/attrs/issues/229)) + +- Added new option *auto_attribs* to `@attr.s` that allows to collect annotated fields without setting them to `attr.ib()`. + + Setting a field to an `attr.ib()` is still possible to supply options like validators. + Setting it to any other value is treated like it was passed as `attr.ib(default=value)` -- passing an instance of `attr.Factory` also works as expected. + ([#262](https://github.com/python-attrs/attrs/issues/262), [#277](https://github.com/python-attrs/attrs/issues/277)) + +- Instances of classes created using `attr.make_class()` can now be pickled. + ([#282](https://github.com/python-attrs/attrs/issues/282)) + +--- + +## [17.2.0](https://github.com/python-attrs/attrs/tree/17.2.0) - 2017-05-24 + +### Changes: + +- Validators are hashable again. + Note that validators may become frozen in the future, pending availability of no-overhead frozen classes. + [#192](https://github.com/python-attrs/attrs/issues/192) + +--- + +## [17.1.0](https://github.com/python-attrs/attrs/tree/17.1.0) - 2017-05-16 + +To encourage more participation, the project has also been moved into a [dedicated GitHub organization](https://github.com/python-attrs/) and everyone is most welcome to join! + +`attrs` also has a logo now! + +```{image} https://www.attrs.org/en/latest/_static/attrs_logo.png +:alt: attrs logo +``` + +### Backward-incompatible Changes: + +- `attrs` will set the `__hash__()` method to `None` by default now. + The way hashes were handled before was in conflict with [Python's specification](https://docs.python.org/3/reference/datamodel.html#object.__hash__). + This *may* break some software although this breakage is most likely just surfacing of latent bugs. + You can always make `attrs` create the `__hash__()` method using `@attr.s(hash=True)`. + See [#136] for the rationale of this change. + + :::{warning} + Please *do not* upgrade blindly and *do* test your software! + *Especially* if you use instances as dict keys or put them into sets! + ::: + +- Correspondingly, `attr.ib`'s *hash* argument is `None` by default too and mirrors the *cmp* argument as it should. + +### Deprecations: + +- `attr.assoc()` is now deprecated in favor of `attr.evolve()` and will stop working in 2018. + +### Changes: + +- Fix default hashing behavior. + Now *hash* mirrors the value of *cmp* and classes are unhashable by default. + [#136] + [#142](https://github.com/python-attrs/attrs/issues/142) +- Added `attr.evolve()` that, given an instance of an `attrs` class and field changes as keyword arguments, will instantiate a copy of the given instance with the changes applied. + `evolve()` replaces `assoc()`, which is now deprecated. + `evolve()` is significantly faster than `assoc()`, and requires the class have an initializer that can take the field values as keyword arguments (like `attrs` itself can generate). + [#116](https://github.com/python-attrs/attrs/issues/116) + [#124](https://github.com/python-attrs/attrs/pull/124) + [#135](https://github.com/python-attrs/attrs/pull/135) +- `FrozenInstanceError` is now raised when trying to delete an attribute from a frozen class. + [#118](https://github.com/python-attrs/attrs/pull/118) +- Frozen-ness of classes is now inherited. + [#128](https://github.com/python-attrs/attrs/pull/128) +- `__attrs_post_init__()` is now run if validation is disabled. + [#130](https://github.com/python-attrs/attrs/pull/130) +- Added `attr.validators.in_(options)` that, given the allowed `options`, checks whether the attribute value is in it. + This can be used to check constants, enums, mappings, etc. + [#181](https://github.com/python-attrs/attrs/pull/181) +- Added `attr.validators.and_()` that composes multiple validators into one. + [#161](https://github.com/python-attrs/attrs/issues/161) +- For convenience, the *validator* argument of `@attr.s` now can take a list of validators that are wrapped using `and_()`. + [#138](https://github.com/python-attrs/attrs/issues/138) +- Accordingly, `attr.validators.optional()` now can take a list of validators too. + [#161](https://github.com/python-attrs/attrs/issues/161) +- Validators can now be defined conveniently inline by using the attribute as a decorator. + Check out the [validator examples](https://www.attrs.org/en/stable/init.html#decorator) to see it in action! + [#143](https://github.com/python-attrs/attrs/issues/143) +- `attr.Factory()` now has a *takes_self* argument that makes the initializer to pass the partially initialized instance into the factory. + In other words you can define attribute defaults based on other attributes. + [#165] + [#189](https://github.com/python-attrs/attrs/issues/189) +- Default factories can now also be defined inline using decorators. + They are *always* passed the partially initialized instance. + [#165] +- Conversion can now be made optional using `attr.converters.optional()`. + [#105](https://github.com/python-attrs/attrs/issues/105) + [#173](https://github.com/python-attrs/attrs/pull/173) +- `attr.make_class()` now accepts the keyword argument `bases` which allows for subclassing. + [#152](https://github.com/python-attrs/attrs/pull/152) +- Metaclasses are now preserved with `slots=True`. + [#155](https://github.com/python-attrs/attrs/pull/155) + +--- + +## [16.3.0](https://github.com/python-attrs/attrs/tree/16.3.0) - 2016-11-24 + +### Changes: + +- Attributes now can have user-defined metadata which greatly improves `attrs`'s extensibility. + [#96](https://github.com/python-attrs/attrs/pull/96) + +- Allow for a `__attrs_post_init__()` method that -- if defined -- will get called at the end of the `attrs`-generated `__init__()` method. + [#111](https://github.com/python-attrs/attrs/pull/111) + +- Added `@attr.s(str=True)` that will optionally create a `__str__()` method that is identical to `__repr__()`. + This is mainly useful with `Exception`s and other classes that rely on a useful `__str__()` implementation but overwrite the default one through a poor own one. + Default Python class behavior is to use `__repr__()` as `__str__()` anyways. + + If you tried using `attrs` with `Exception`s and were puzzled by the tracebacks: this option is for you. + +- `__name__` is no longer overwritten with `__qualname__` for `attr.s(slots=True)` classes. + [#99](https://github.com/python-attrs/attrs/issues/99) + +--- + +## [16.2.0](https://github.com/python-attrs/attrs/tree/16.2.0) - 2016-09-17 + +### Changes: + +- Added `attr.astuple()` that -- similarly to `attr.asdict()` -- returns the instance as a tuple. + [#77](https://github.com/python-attrs/attrs/issues/77) +- Converters now work with frozen classes. + [#76](https://github.com/python-attrs/attrs/issues/76) +- Instantiation of `attrs` classes with converters is now significantly faster. + [#80](https://github.com/python-attrs/attrs/pull/80) +- Pickling now works with slotted classes. + [#81](https://github.com/python-attrs/attrs/issues/81) +- `attr.assoc()` now works with slotted classes. + [#84](https://github.com/python-attrs/attrs/issues/84) +- The tuple returned by `attr.fields()` now also allows to access the `Attribute` instances by name. + Yes, we've subclassed `tuple` so you don't have to! + Therefore `attr.fields(C).x` is equivalent to the deprecated `C.x` and works with slotted classes. + [#88](https://github.com/python-attrs/attrs/issues/88) + +--- + +## [16.1.0](https://github.com/python-attrs/attrs/tree/16.1.0) - 2016-08-30 + +### Backward-incompatible Changes: + +- All instances where function arguments were called `cl` have been changed to the more Pythonic `cls`. + Since it was always the first argument, it's doubtful anyone ever called those function with in the keyword form. + If so, sorry for any breakage but there's no practical deprecation path to solve this ugly wart. + +### Deprecations: + +- Accessing `Attribute` instances on class objects is now deprecated and will stop working in 2017. + If you need introspection please use the `__attrs_attrs__` attribute or the `attr.fields()` function that carry them too. + In the future, the attributes that are defined on the class body and are usually overwritten in your `__init__` method are simply removed after `@attr.s` has been applied. + + This will remove the confusing error message if you write your own `__init__` and forget to initialize some attribute. + Instead you will get a straightforward `AttributeError`. + In other words: decorated classes will work more like plain Python classes which was always `attrs`'s goal. + +- The serious-business aliases `attr.attributes` and `attr.attr` have been deprecated in favor of `attr.attrs` and `attr.attrib` which are much more consistent and frankly obvious in hindsight. + They will be purged from documentation immediately but there are no plans to actually remove them. + +### Changes: + +- `attr.asdict()`'s `dict_factory` arguments is now propagated on recursion. + [#45](https://github.com/python-attrs/attrs/issues/45) +- `attr.asdict()`, `attr.has()` and `attr.fields()` are significantly faster. + [#48](https://github.com/python-attrs/attrs/issues/48) + [#51](https://github.com/python-attrs/attrs/issues/51) +- Add `attr.attrs` and `attr.attrib` as a more consistent aliases for `attr.s` and `attr.ib`. +- Add *frozen* option to `attr.s` that will make instances best-effort immutable. + [#60](https://github.com/python-attrs/attrs/issues/60) +- `attr.asdict()` now takes `retain_collection_types` as an argument. + If `True`, it does not convert attributes of type `tuple` or `set` to `list`. + [#69](https://github.com/python-attrs/attrs/issues/69) + +--- + +## [16.0.0](https://github.com/python-attrs/attrs/tree/16.0.0) - 2016-05-23 + +### Backward-incompatible Changes: + +- Python 3.3 and 2.6 are no longer supported. + They may work by chance but any effort to keep them working has ceased. + + The last Python 2.6 release was on October 29, 2013 and is no longer supported by the CPython core team. + Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. + + Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. + +### Changes: + +- `__slots__` have arrived! + Classes now can automatically be [slotted](https://docs.python.org/3/reference/datamodel.html#slots)-style (and save your precious memory) just by passing `slots=True`. + [#35](https://github.com/python-attrs/attrs/issues/35) +- Allow the case of initializing attributes that are set to `init=False`. + This allows for clean initializer parameter lists while being able to initialize attributes to default values. + [#32](https://github.com/python-attrs/attrs/issues/32) +- `attr.asdict()` can now produce arbitrary mappings instead of Python `dict`s when provided with a `dict_factory` argument. + [#40](https://github.com/python-attrs/attrs/issues/40) +- Multiple performance improvements. + +--- + +## [15.2.0](https://github.com/python-attrs/attrs/tree/15.2.0) - 2015-12-08 + +### Changes: + +- Added a `convert` argument to `attr.ib`, which allows specifying a function to run on arguments. + This allows for simple type conversions, e.g. with `attr.ib(convert=int)`. + [#26](https://github.com/python-attrs/attrs/issues/26) +- Speed up object creation when attribute validators are used. + [#28](https://github.com/python-attrs/attrs/issues/28) + +--- + +## [15.1.0](https://github.com/python-attrs/attrs/tree/15.1.0) - 2015-08-20 + +### Changes: + +- Added `attr.validators.optional()` that wraps other validators allowing attributes to be `None`. + [#16](https://github.com/python-attrs/attrs/issues/16) +- Multi-level inheritance now works. + [#24](https://github.com/python-attrs/attrs/issues/24) +- `__repr__()` now works with non-redecorated subclasses. + [#20](https://github.com/python-attrs/attrs/issues/20) + +--- + +## [15.0.0](https://github.com/python-attrs/attrs/tree/15.0.0) - 2015-04-15 + +### Changes: + +Initial release. + +[#136]: https://github.com/python-attrs/attrs/issues/136 +[#165]: https://github.com/python-attrs/attrs/issues/165 +[#425]: https://github.com/python-attrs/attrs/issues/425 diff --git a/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.rst b/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.rst deleted file mode 100644 index 1d194add22b..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/CHANGELOG.rst +++ /dev/null @@ -1,1027 +0,0 @@ -Changelog -========= - -Versions follow `CalVer <https://calver.org>`_ with a strict backwards-compatibility policy. - -The **first number** of the version is the year. -The **second number** is incremented with each release, starting at 1 for each year. -The **third number** is when we need to start branches for older releases (only for emergencies). - -Put simply, you shouldn't ever be afraid to upgrade ``attrs`` if you're only using its public APIs. -Whenever there is a need to break compatibility, it is announced here in the changelog, and raises a ``DeprecationWarning`` for a year (if possible) before it's finally really broken. - -.. warning:: - - The structure of the `attrs.Attribute` class is exempt from this rule. - It *will* change in the future, but since it should be considered read-only, that shouldn't matter. - - However if you intend to build extensions on top of ``attrs`` you have to anticipate that. - -.. towncrier release notes start - -21.4.0 (2021-12-29) -------------------- - -Changes -^^^^^^^ - -- Fixed the test suite on PyPy3.8 where ``cloudpickle`` does not work. - `#892 <https://github.com/python-attrs/attrs/issues/892>`_ -- Fixed ``coverage report`` for projects that use ``attrs`` and don't set a ``--source``. - `#895 <https://github.com/python-attrs/attrs/issues/895>`_, - `#896 <https://github.com/python-attrs/attrs/issues/896>`_ - - ----- - - -21.3.0 (2021-12-28) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- When using ``@define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. - I.e. the new default is ``on_setattr=[attrs.setters.convert, attrs.setters.validate]``. - - This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. - `#835 <https://github.com/python-attrs/attrs/issues/835>`_, - `#886 <https://github.com/python-attrs/attrs/issues/886>`_ -- ``import attrs`` has finally landed! - As of this release, you can finally import ``attrs`` using its proper name. - - Not all names from the ``attr`` namespace have been transferred; most notably ``attr.s`` and ``attr.ib`` are missing. - See ``attrs.define`` and ``attrs.field`` if you haven't seen our next-generation APIs yet. - A more elaborate explanation can be found `On The Core API Names <https://www.attrs.org/en/latest/names.html>`_ - - This feature is at least for one release **provisional**. - We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. - - The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. - Please note that we have **not** moved -- or even removed -- anything from ``attr``! - - Please do report any bugs or documentation inconsistencies! - `#887 <https://github.com/python-attrs/attrs/issues/887>`_ - - -Changes -^^^^^^^ - -- ``attr.asdict(retain_collection_types=False)`` (default) dumps collection-esque keys as tuples. - `#646 <https://github.com/python-attrs/attrs/issues/646>`_, - `#888 <https://github.com/python-attrs/attrs/issues/888>`_ -- ``__match_args__`` are now generated to support Python 3.10's - `Structural Pattern Matching <https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching>`_. - This can be controlled by the ``match_args`` argument to the class decorators on Python 3.10 and later. - On older versions, it is never added and the argument is ignored. - `#815 <https://github.com/python-attrs/attrs/issues/815>`_ -- If the class-level *on_setattr* is set to ``attrs.setters.validate`` (default in ``@define`` and ``@mutable``) but no field defines a validator, pretend that it's not set. - `#817 <https://github.com/python-attrs/attrs/issues/817>`_ -- The generated ``__repr__`` is significantly faster on Pythons with f-strings. - `#819 <https://github.com/python-attrs/attrs/issues/819>`_ -- Attributes transformed via ``field_transformer`` are wrapped with ``AttrsClass`` again. - `#824 <https://github.com/python-attrs/attrs/issues/824>`_ -- Generated source code is now cached more efficiently for identical classes. - `#828 <https://github.com/python-attrs/attrs/issues/828>`_ -- Added ``attrs.converters.to_bool()``. - `#830 <https://github.com/python-attrs/attrs/issues/830>`_ -- ``attrs.resolve_types()`` now resolves types of subclasses after the parents are resolved. - `#842 <https://github.com/python-attrs/attrs/issues/842>`_ - `#843 <https://github.com/python-attrs/attrs/issues/843>`_ -- Added new validators: ``lt(val)`` (< val), ``le(va)`` (≤ val), ``ge(val)`` (≥ val), ``gt(val)`` (> val), and ``maxlen(n)``. - `#845 <https://github.com/python-attrs/attrs/issues/845>`_ -- ``attrs`` classes are now fully compatible with `cloudpickle <https://github.com/cloudpipe/cloudpickle>`_ (no need to disable ``repr`` anymore). - `#857 <https://github.com/python-attrs/attrs/issues/857>`_ -- Added new context manager ``attrs.validators.disabled()`` and functions ``attrs.validators.(set|get)_disabled()``. - They deprecate ``attrs.(set|get)_run_validators()``. - All functions are interoperable and modify the same internal state. - They are not – and never were – thread-safe, though. - `#859 <https://github.com/python-attrs/attrs/issues/859>`_ -- ``attrs.validators.matches_re()`` now accepts pre-compiled regular expressions in addition to pattern strings. - `#877 <https://github.com/python-attrs/attrs/issues/877>`_ - - ----- - - -21.2.0 (2021-05-07) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- We had to revert the recursive feature for ``attr.evolve()`` because it broke some use-cases -- sorry! - `#806 <https://github.com/python-attrs/attrs/issues/806>`_ -- Python 3.4 is now blocked using packaging metadata because ``attrs`` can't be imported on it anymore. - To ensure that 3.4 users can keep installing ``attrs`` easily, we will `yank <https://pypi.org/help/#yanked>`_ 21.1.0 from PyPI. - This has **no** consequences if you pin ``attrs`` to 21.1.0. - `#807 <https://github.com/python-attrs/attrs/issues/807>`_ - - ----- - - -21.1.0 (2021-05-06) -------------------- - -Deprecations -^^^^^^^^^^^^ - -- The long-awaited, much-talked-about, little-delivered ``import attrs`` is finally upon us! - - Since the NG APIs have now been proclaimed stable, the **next** release of ``attrs`` will allow you to actually ``import attrs``. - We're taking this opportunity to replace some defaults in our APIs that made sense in 2015, but don't in 2021. - - So please, if you have any pet peeves about defaults in ``attrs``'s APIs, *now* is the time to air your grievances in #487! - We're not gonna get such a chance for a second time, without breaking our backward-compatibility guarantees, or long deprecation cycles. - Therefore, speak now or forever hold you peace! - `#487 <https://github.com/python-attrs/attrs/issues/487>`_ -- The *cmp* argument to ``attr.s()`` and `attr.ib()` has been **undeprecated** - It will continue to be supported as syntactic sugar to set *eq* and *order* in one go. - - I'm terribly sorry for the hassle around this argument! - The reason we're bringing it back is it's usefulness regarding customization of equality/ordering. - - The ``cmp`` attribute and argument on ``attr.Attribute`` remains deprecated and will be removed later this year. - `#773 <https://github.com/python-attrs/attrs/issues/773>`_ - - -Changes -^^^^^^^ - -- It's now possible to customize the behavior of ``eq`` and ``order`` by passing in a callable. - `#435 <https://github.com/python-attrs/attrs/issues/435>`_, - `#627 <https://github.com/python-attrs/attrs/issues/627>`_ -- The instant favorite next-generation APIs are not provisional anymore! - - They are also officially supported by Mypy as of their `0.800 release <https://mypy-lang.blogspot.com/2021/01/mypy-0800-released.html>`_. - - We hope the next release will already contain an (additional) importable package called ``attrs``. - `#668 <https://github.com/python-attrs/attrs/issues/668>`_, - `#786 <https://github.com/python-attrs/attrs/issues/786>`_ -- If an attribute defines a converter, the type of its parameter is used as type annotation for its corresponding ``__init__`` parameter. - - If an ``attr.converters.pipe`` is used, the first one's is used. - `#710 <https://github.com/python-attrs/attrs/issues/710>`_ -- Fixed the creation of an extra slot for an ``attr.ib`` when the parent class already has a slot with the same name. - `#718 <https://github.com/python-attrs/attrs/issues/718>`_ -- ``__attrs__init__()`` will now be injected if ``init=False``, or if ``auto_detect=True`` and a user-defined ``__init__()`` exists. - - This enables users to do "pre-init" work in their ``__init__()`` (such as ``super().__init__()``). - - ``__init__()`` can then delegate constructor argument processing to ``self.__attrs_init__(*args, **kwargs)``. - `#731 <https://github.com/python-attrs/attrs/issues/731>`_ -- ``bool(attr.NOTHING)`` is now ``False``. - `#732 <https://github.com/python-attrs/attrs/issues/732>`_ -- It's now possible to use ``super()`` inside of properties of slotted classes. - `#747 <https://github.com/python-attrs/attrs/issues/747>`_ -- Allow for a ``__attrs_pre_init__()`` method that -- if defined -- will get called at the beginning of the ``attrs``-generated ``__init__()`` method. - `#750 <https://github.com/python-attrs/attrs/issues/750>`_ -- Added forgotten ``attr.Attribute.evolve()`` to type stubs. - `#752 <https://github.com/python-attrs/attrs/issues/752>`_ -- ``attrs.evolve()`` now works recursively with nested ``attrs`` classes. - `#759 <https://github.com/python-attrs/attrs/issues/759>`_ -- Python 3.10 is now officially supported. - `#763 <https://github.com/python-attrs/attrs/issues/763>`_ -- ``attr.resolve_types()`` now takes an optional *attrib* argument to work inside a ``field_transformer``. - `#774 <https://github.com/python-attrs/attrs/issues/774>`_ -- ``ClassVar``\ s are now also detected if they come from `typing-extensions <https://pypi.org/project/typing-extensions/>`_. - `#782 <https://github.com/python-attrs/attrs/issues/782>`_ -- To make it easier to customize attribute comparison (#435), we have added the ``attr.cmp_with()`` helper. - - See the `new docs on comparison <https://www.attrs.org/en/stable/comparison.html>`_ for more details. - `#787 <https://github.com/python-attrs/attrs/issues/787>`_ -- Added **provisional** support for static typing in ``pyright`` via the `dataclass_transforms specification <https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md>`_. - Both the ``pyright`` specification and ``attrs`` implementation may change in future versions of both projects. - - Your constructive feedback is welcome in both `attrs#795 <https://github.com/python-attrs/attrs/issues/795>`_ and `pyright#1782 <https://github.com/microsoft/pyright/discussions/1782>`_. - `#796 <https://github.com/python-attrs/attrs/issues/796>`_ - - ----- - - -20.3.0 (2020-11-05) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- ``attr.define()``, ``attr.frozen()``, ``attr.mutable()``, and ``attr.field()`` remain **provisional**. - - This release does **not** change anything about them and they are already used widely in production though. - - If you wish to use them together with mypy, you can simply drop `this plugin <https://gist.github.com/hynek/1e3844d0c99e479e716169034b5fa963#file-attrs_ng_plugin-py>`_ into your project. - - Feel free to provide feedback to them in the linked issue #668. - - We will release the ``attrs`` namespace once we have the feeling that the APIs have properly settled. - `#668 <https://github.com/python-attrs/attrs/issues/668>`_ - - -Changes -^^^^^^^ - -- ``attr.s()`` now has a *field_transformer* hook that is called for all ``Attribute``\ s and returns a (modified or updated) list of ``Attribute`` instances. - ``attr.asdict()`` has a *value_serializer* hook that can change the way values are converted. - Both hooks are meant to help with data (de-)serialization workflows. - `#653 <https://github.com/python-attrs/attrs/issues/653>`_ -- ``kw_only=True`` now works on Python 2. - `#700 <https://github.com/python-attrs/attrs/issues/700>`_ -- ``raise from`` now works on frozen classes on PyPy. - `#703 <https://github.com/python-attrs/attrs/issues/703>`_, - `#712 <https://github.com/python-attrs/attrs/issues/712>`_ -- ``attr.asdict()`` and ``attr.astuple()`` now treat ``frozenset``\ s like ``set``\ s with regards to the *retain_collection_types* argument. - `#704 <https://github.com/python-attrs/attrs/issues/704>`_ -- The type stubs for ``attr.s()`` and ``attr.make_class()`` are not missing the *collect_by_mro* argument anymore. - `#711 <https://github.com/python-attrs/attrs/issues/711>`_ - - ----- - - -20.2.0 (2020-09-05) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- ``attr.define()``, ``attr.frozen()``, ``attr.mutable()``, and ``attr.field()`` remain **provisional**. - - This release fixes a bunch of bugs and ergonomics but they remain mostly unchanged. - - If you wish to use them together with mypy, you can simply drop `this plugin <https://gist.github.com/hynek/1e3844d0c99e479e716169034b5fa963#file-attrs_ng_plugin-py>`_ into your project. - - Feel free to provide feedback to them in the linked issue #668. - - We will release the ``attrs`` namespace once we have the feeling that the APIs have properly settled. - `#668 <https://github.com/python-attrs/attrs/issues/668>`_ - - -Changes -^^^^^^^ - -- ``attr.define()`` et al now correct detect ``__eq__`` and ``__ne__``. - `#671 <https://github.com/python-attrs/attrs/issues/671>`_ -- ``attr.define()`` et al's hybrid behavior now also works correctly when arguments are passed. - `#675 <https://github.com/python-attrs/attrs/issues/675>`_ -- It's possible to define custom ``__setattr__`` methods on slotted classes again. - `#681 <https://github.com/python-attrs/attrs/issues/681>`_ -- In 20.1.0 we introduced the ``inherited`` attribute on the ``attr.Attribute`` class to differentiate attributes that have been inherited and those that have been defined directly on the class. - - It has shown to be problematic to involve that attribute when comparing instances of ``attr.Attribute`` though, because when sub-classing, attributes from base classes are suddenly not equal to themselves in a super class. - - Therefore the ``inherited`` attribute will now be ignored when hashing and comparing instances of ``attr.Attribute``. - `#684 <https://github.com/python-attrs/attrs/issues/684>`_ -- ``zope.interface`` is now a "soft dependency" when running the test suite; if ``zope.interface`` is not installed when running the test suite, the interface-related tests will be automatically skipped. - `#685 <https://github.com/python-attrs/attrs/issues/685>`_ -- The ergonomics of creating frozen classes using ``@define(frozen=True)`` and sub-classing frozen classes has been improved: - you don't have to set ``on_setattr=None`` anymore. - `#687 <https://github.com/python-attrs/attrs/issues/687>`_ - - ----- - - -20.1.0 (2020-08-20) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Python 3.4 is not supported anymore. - It has been unsupported by the Python core team for a while now, its PyPI downloads are negligible, and our CI provider removed it as a supported option. - - It's very unlikely that ``attrs`` will break under 3.4 anytime soon, which is why we do *not* block its installation on Python 3.4. - But we don't test it anymore and will block it once someone reports breakage. - `#608 <https://github.com/python-attrs/attrs/issues/608>`_ - - -Deprecations -^^^^^^^^^^^^ - -- Less of a deprecation and more of a heads up: the next release of ``attrs`` will introduce an ``attrs`` namespace. - That means that you'll finally be able to run ``import attrs`` with new functions that aren't cute abbreviations and that will carry better defaults. - - This should not break any of your code, because project-local packages have priority before installed ones. - If this is a problem for you for some reason, please report it to our bug tracker and we'll figure something out. - - The old ``attr`` namespace isn't going anywhere and its defaults are not changing – this is a purely additive measure. - Please check out the linked issue for more details. - - These new APIs have been added *provisionally* as part of #666 so you can try them out today and provide feedback. - Learn more in the `API docs <https://www.attrs.org/en/stable/api.html>`_. - `#408 <https://github.com/python-attrs/attrs/issues/408>`_ - - -Changes -^^^^^^^ - -- Added ``attr.resolve_types()``. - It ensures that all forward-references and types in string form are resolved into concrete types. - - You need this only if you need concrete types at runtime. - That means that if you only use types for static type checking, you do **not** need this function. - `#288 <https://github.com/python-attrs/attrs/issues/288>`_, - `#302 <https://github.com/python-attrs/attrs/issues/302>`_ -- Added ``@attr.s(collect_by_mro=False)`` argument that if set to ``True`` fixes the collection of attributes from base classes. - - It's only necessary for certain cases of multiple-inheritance but is kept off for now for backward-compatibility reasons. - It will be turned on by default in the future. - - As a side-effect, ``attr.Attribute`` now *always* has an ``inherited`` attribute indicating whether an attribute on a class was directly defined or inherited. - `#428 <https://github.com/python-attrs/attrs/issues/428>`_, - `#635 <https://github.com/python-attrs/attrs/issues/635>`_ -- On Python 3, all generated methods now have a docstring explaining that they have been created by ``attrs``. - `#506 <https://github.com/python-attrs/attrs/issues/506>`_ -- It is now possible to prevent ``attrs`` from auto-generating the ``__setstate__`` and ``__getstate__`` methods that are required for pickling of slotted classes. - - Either pass ``@attr.s(getstate_setstate=False)`` or pass ``@attr.s(auto_detect=True)`` and implement them yourself: - if ``attrs`` finds either of the two methods directly on the decorated class, it assumes implicitly ``getstate_setstate=False`` (and implements neither). - - This option works with dict classes but should never be necessary. - `#512 <https://github.com/python-attrs/attrs/issues/512>`_, - `#513 <https://github.com/python-attrs/attrs/issues/513>`_, - `#642 <https://github.com/python-attrs/attrs/issues/642>`_ -- Fixed a ``ValueError: Cell is empty`` bug that could happen in some rare edge cases. - `#590 <https://github.com/python-attrs/attrs/issues/590>`_ -- ``attrs`` can now automatically detect your own implementations and infer ``init=False``, ``repr=False``, ``eq=False``, ``order=False``, and ``hash=False`` if you set ``@attr.s(auto_detect=True)``. - ``attrs`` will ignore inherited methods. - If the argument implies more than one method (e.g. ``eq=True`` creates both ``__eq__`` and ``__ne__``), it's enough for *one* of them to exist and ``attrs`` will create *neither*. - - This feature requires Python 3. - `#607 <https://github.com/python-attrs/attrs/issues/607>`_ -- Added ``attr.converters.pipe()``. - The feature allows combining multiple conversion callbacks into one by piping the value through all of them, and retuning the last result. - - As part of this feature, we had to relax the type information for converter callables. - `#618 <https://github.com/python-attrs/attrs/issues/618>`_ -- Fixed serialization behavior of non-slots classes with ``cache_hash=True``. - The hash cache will be cleared on operations which make "deep copies" of instances of classes with hash caching, - though the cache will not be cleared with shallow copies like those made by ``copy.copy()``. - - Previously, ``copy.deepcopy()`` or serialization and deserialization with ``pickle`` would result in an un-initialized object. - - This change also allows the creation of ``cache_hash=True`` classes with a custom ``__setstate__``, - which was previously forbidden (`#494 <https://github.com/python-attrs/attrs/issues/494>`_). - `#620 <https://github.com/python-attrs/attrs/issues/620>`_ -- It is now possible to specify hooks that are called whenever an attribute is set **after** a class has been instantiated. - - You can pass ``on_setattr`` both to ``@attr.s()`` to set the default for all attributes on a class, and to ``@attr.ib()`` to overwrite it for individual attributes. - - ``attrs`` also comes with a new module ``attr.setters`` that brings helpers that run validators, converters, or allow to freeze a subset of attributes. - `#645 <https://github.com/python-attrs/attrs/issues/645>`_, - `#660 <https://github.com/python-attrs/attrs/issues/660>`_ -- **Provisional** APIs called ``attr.define()``, ``attr.mutable()``, and ``attr.frozen()`` have been added. - - They are only available on Python 3.6 and later, and call ``attr.s()`` with different default values. - - If nothing comes up, they will become the official way for creating classes in 20.2.0 (see above). - - **Please note** that it may take some time until mypy – and other tools that have dedicated support for ``attrs`` – recognize these new APIs. - Please **do not** open issues on our bug tracker, there is nothing we can do about it. - `#666 <https://github.com/python-attrs/attrs/issues/666>`_ -- We have also provisionally added ``attr.field()`` that supplants ``attr.ib()``. - It also requires at least Python 3.6 and is keyword-only. - Other than that, it only dropped a few arguments, but changed no defaults. - - As with ``attr.s()``: ``attr.ib()`` is not going anywhere. - `#669 <https://github.com/python-attrs/attrs/issues/669>`_ - - ----- - - -19.3.0 (2019-10-15) -------------------- - -Changes -^^^^^^^ - -- Fixed ``auto_attribs`` usage when default values cannot be compared directly with ``==``, such as ``numpy`` arrays. - `#585 <https://github.com/python-attrs/attrs/issues/585>`_ - - ----- - - -19.2.0 (2019-10-01) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Removed deprecated ``Attribute`` attribute ``convert`` per scheduled removal on 2019/1. - This planned deprecation is tracked in issue `#307 <https://github.com/python-attrs/attrs/issues/307>`_. - `#504 <https://github.com/python-attrs/attrs/issues/504>`_ -- ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` do not consider subclasses comparable anymore. - - This has been deprecated since 18.2.0 and was raising a ``DeprecationWarning`` for over a year. - `#570 <https://github.com/python-attrs/attrs/issues/570>`_ - - -Deprecations -^^^^^^^^^^^^ - -- The ``cmp`` argument to ``attr.s()`` and ``attr.ib()`` is now deprecated. - - Please use ``eq`` to add equality methods (``__eq__`` and ``__ne__``) and ``order`` to add ordering methods (``__lt__``, ``__le__``, ``__gt__``, and ``__ge__``) instead – just like with `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_. - - Both are effectively ``True`` by default but it's enough to set ``eq=False`` to disable both at once. - Passing ``eq=False, order=True`` explicitly will raise a ``ValueError`` though. - - Since this is arguably a deeper backward-compatibility break, it will have an extended deprecation period until 2021-06-01. - After that day, the ``cmp`` argument will be removed. - - ``attr.Attribute`` also isn't orderable anymore. - `#574 <https://github.com/python-attrs/attrs/issues/574>`_ - - -Changes -^^^^^^^ - -- Updated ``attr.validators.__all__`` to include new validators added in `#425`_. - `#517 <https://github.com/python-attrs/attrs/issues/517>`_ -- Slotted classes now use a pure Python mechanism to rewrite the ``__class__`` cell when rebuilding the class, so ``super()`` works even on environments where ``ctypes`` is not installed. - `#522 <https://github.com/python-attrs/attrs/issues/522>`_ -- When collecting attributes using ``@attr.s(auto_attribs=True)``, attributes with a default of ``None`` are now deleted too. - `#523 <https://github.com/python-attrs/attrs/issues/523>`_, - `#556 <https://github.com/python-attrs/attrs/issues/556>`_ -- Fixed ``attr.validators.deep_iterable()`` and ``attr.validators.deep_mapping()`` type stubs. - `#533 <https://github.com/python-attrs/attrs/issues/533>`_ -- ``attr.validators.is_callable()`` validator now raises an exception ``attr.exceptions.NotCallableError``, a subclass of ``TypeError``, informing the received value. - `#536 <https://github.com/python-attrs/attrs/issues/536>`_ -- ``@attr.s(auto_exc=True)`` now generates classes that are hashable by ID, as the documentation always claimed it would. - `#543 <https://github.com/python-attrs/attrs/issues/543>`_, - `#563 <https://github.com/python-attrs/attrs/issues/563>`_ -- Added ``attr.validators.matches_re()`` that checks string attributes whether they match a regular expression. - `#552 <https://github.com/python-attrs/attrs/issues/552>`_ -- Keyword-only attributes (``kw_only=True``) and attributes that are excluded from the ``attrs``'s ``__init__`` (``init=False``) now can appear before mandatory attributes. - `#559 <https://github.com/python-attrs/attrs/issues/559>`_ -- The fake filename for generated methods is now more stable. - It won't change when you restart the process. - `#560 <https://github.com/python-attrs/attrs/issues/560>`_ -- The value passed to ``@attr.ib(repr=…)`` can now be either a boolean (as before) or a callable. - That callable must return a string and is then used for formatting the attribute by the generated ``__repr__()`` method. - `#568 <https://github.com/python-attrs/attrs/issues/568>`_ -- Added ``attr.__version_info__`` that can be used to reliably check the version of ``attrs`` and write forward- and backward-compatible code. - Please check out the `section on deprecated APIs <http://www.attrs.org/en/stable/api.html#deprecated-apis>`_ on how to use it. - `#580 <https://github.com/python-attrs/attrs/issues/580>`_ - - .. _`#425`: https://github.com/python-attrs/attrs/issues/425 - - ----- - - -19.1.0 (2019-03-03) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Fixed a bug where deserialized objects with ``cache_hash=True`` could have incorrect hash code values. - This change breaks classes with ``cache_hash=True`` when a custom ``__setstate__`` is present. - An exception will be thrown when applying the ``attrs`` annotation to such a class. - This limitation is tracked in issue `#494 <https://github.com/python-attrs/attrs/issues/494>`_. - `#482 <https://github.com/python-attrs/attrs/issues/482>`_ - - -Changes -^^^^^^^ - -- Add ``is_callable``, ``deep_iterable``, and ``deep_mapping`` validators. - - * ``is_callable``: validates that a value is callable - * ``deep_iterable``: Allows recursion down into an iterable, - applying another validator to every member in the iterable - as well as applying an optional validator to the iterable itself. - * ``deep_mapping``: Allows recursion down into the items in a mapping object, - applying a key validator and a value validator to the key and value in every item. - Also applies an optional validator to the mapping object itself. - - You can find them in the ``attr.validators`` package. - `#425`_ -- Fixed stub files to prevent errors raised by mypy's ``disallow_any_generics = True`` option. - `#443 <https://github.com/python-attrs/attrs/issues/443>`_ -- Attributes with ``init=False`` now can follow after ``kw_only=True`` attributes. - `#450 <https://github.com/python-attrs/attrs/issues/450>`_ -- ``attrs`` now has first class support for defining exception classes. - - If you define a class using ``@attr.s(auto_exc=True)`` and subclass an exception, the class will behave like a well-behaved exception class including an appropriate ``__str__`` method, and all attributes additionally available in an ``args`` attribute. - `#500 <https://github.com/python-attrs/attrs/issues/500>`_ -- Clarified documentation for hashing to warn that hashable objects should be deeply immutable (in their usage, even if this is not enforced). - `#503 <https://github.com/python-attrs/attrs/issues/503>`_ - - ----- - - -18.2.0 (2018-09-01) -------------------- - -Deprecations -^^^^^^^^^^^^ - -- Comparing subclasses using ``<``, ``>``, ``<=``, and ``>=`` is now deprecated. - The docs always claimed that instances are only compared if the types are identical, so this is a first step to conform to the docs. - - Equality operators (``==`` and ``!=``) were always strict in this regard. - `#394 <https://github.com/python-attrs/attrs/issues/394>`_ - - -Changes -^^^^^^^ - -- ``attrs`` now ships its own `PEP 484 <https://www.python.org/dev/peps/pep-0484/>`_ type hints. - Together with `mypy <http://mypy-lang.org>`_'s ``attrs`` plugin, you've got all you need for writing statically typed code in both Python 2 and 3! - - At that occasion, we've also added `narrative docs <https://www.attrs.org/en/stable/types.html>`_ about type annotations in ``attrs``. - `#238 <https://github.com/python-attrs/attrs/issues/238>`_ -- Added *kw_only* arguments to ``attr.ib`` and ``attr.s``, and a corresponding *kw_only* attribute to ``attr.Attribute``. - This change makes it possible to have a generated ``__init__`` with keyword-only arguments on Python 3, relaxing the required ordering of default and non-default valued attributes. - `#281 <https://github.com/python-attrs/attrs/issues/281>`_, - `#411 <https://github.com/python-attrs/attrs/issues/411>`_ -- The test suite now runs with ``hypothesis.HealthCheck.too_slow`` disabled to prevent CI breakage on slower computers. - `#364 <https://github.com/python-attrs/attrs/issues/364>`_, - `#396 <https://github.com/python-attrs/attrs/issues/396>`_ -- ``attr.validators.in_()`` now raises a ``ValueError`` with a useful message even if the options are a string and the value is not a string. - `#383 <https://github.com/python-attrs/attrs/issues/383>`_ -- ``attr.asdict()`` now properly handles deeply nested lists and dictionaries. - `#395 <https://github.com/python-attrs/attrs/issues/395>`_ -- Added ``attr.converters.default_if_none()`` that allows to replace ``None`` values in attributes. - For example ``attr.ib(converter=default_if_none(""))`` replaces ``None`` by empty strings. - `#400 <https://github.com/python-attrs/attrs/issues/400>`_, - `#414 <https://github.com/python-attrs/attrs/issues/414>`_ -- Fixed a reference leak where the original class would remain live after being replaced when ``slots=True`` is set. - `#407 <https://github.com/python-attrs/attrs/issues/407>`_ -- Slotted classes can now be made weakly referenceable by passing ``@attr.s(weakref_slot=True)``. - `#420 <https://github.com/python-attrs/attrs/issues/420>`_ -- Added *cache_hash* option to ``@attr.s`` which causes the hash code to be computed once and stored on the object. - `#426 <https://github.com/python-attrs/attrs/issues/426>`_ -- Attributes can be named ``property`` and ``itemgetter`` now. - `#430 <https://github.com/python-attrs/attrs/issues/430>`_ -- It is now possible to override a base class' class variable using only class annotations. - `#431 <https://github.com/python-attrs/attrs/issues/431>`_ - - ----- - - -18.1.0 (2018-05-03) -------------------- - -Changes -^^^^^^^ - -- ``x=X(); x.cycle = x; repr(x)`` will no longer raise a ``RecursionError``, and will instead show as ``X(x=...)``. - - `#95 <https://github.com/python-attrs/attrs/issues/95>`_ -- ``attr.ib(factory=f)`` is now syntactic sugar for the common case of ``attr.ib(default=attr.Factory(f))``. - - `#178 <https://github.com/python-attrs/attrs/issues/178>`_, - `#356 <https://github.com/python-attrs/attrs/issues/356>`_ -- Added ``attr.field_dict()`` to return an ordered dictionary of ``attrs`` attributes for a class, whose keys are the attribute names. - - `#290 <https://github.com/python-attrs/attrs/issues/290>`_, - `#349 <https://github.com/python-attrs/attrs/issues/349>`_ -- The order of attributes that are passed into ``attr.make_class()`` or the *these* argument of ``@attr.s()`` is now retained if the dictionary is ordered (i.e. ``dict`` on Python 3.6 and later, ``collections.OrderedDict`` otherwise). - - Before, the order was always determined by the order in which the attributes have been defined which may not be desirable when creating classes programatically. - - `#300 <https://github.com/python-attrs/attrs/issues/300>`_, - `#339 <https://github.com/python-attrs/attrs/issues/339>`_, - `#343 <https://github.com/python-attrs/attrs/issues/343>`_ -- In slotted classes, ``__getstate__`` and ``__setstate__`` now ignore the ``__weakref__`` attribute. - - `#311 <https://github.com/python-attrs/attrs/issues/311>`_, - `#326 <https://github.com/python-attrs/attrs/issues/326>`_ -- Setting the cell type is now completely best effort. - This fixes ``attrs`` on Jython. - - We cannot make any guarantees regarding Jython though, because our test suite cannot run due to dependency incompatabilities. - - `#321 <https://github.com/python-attrs/attrs/issues/321>`_, - `#334 <https://github.com/python-attrs/attrs/issues/334>`_ -- If ``attr.s`` is passed a *these* argument, it will no longer attempt to remove attributes with the same name from the class body. - - `#322 <https://github.com/python-attrs/attrs/issues/322>`_, - `#323 <https://github.com/python-attrs/attrs/issues/323>`_ -- The hash of ``attr.NOTHING`` is now vegan and faster on 32bit Python builds. - - `#331 <https://github.com/python-attrs/attrs/issues/331>`_, - `#332 <https://github.com/python-attrs/attrs/issues/332>`_ -- The overhead of instantiating frozen dict classes is virtually eliminated. - `#336 <https://github.com/python-attrs/attrs/issues/336>`_ -- Generated ``__init__`` methods now have an ``__annotations__`` attribute derived from the types of the fields. - - `#363 <https://github.com/python-attrs/attrs/issues/363>`_ -- We have restructured the documentation a bit to account for ``attrs``' growth in scope. - Instead of putting everything into the `examples <https://www.attrs.org/en/stable/examples.html>`_ page, we have started to extract narrative chapters. - - So far, we've added chapters on `initialization <https://www.attrs.org/en/stable/init.html>`_ and `hashing <https://www.attrs.org/en/stable/hashing.html>`_. - - Expect more to come! - - `#369 <https://github.com/python-attrs/attrs/issues/369>`_, - `#370 <https://github.com/python-attrs/attrs/issues/370>`_ - - ----- - - -17.4.0 (2017-12-30) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- The traversal of MROs when using multiple inheritance was backward: - If you defined a class ``C`` that subclasses ``A`` and ``B`` like ``C(A, B)``, ``attrs`` would have collected the attributes from ``B`` *before* those of ``A``. - - This is now fixed and means that in classes that employ multiple inheritance, the output of ``__repr__`` and the order of positional arguments in ``__init__`` changes. - Because of the nature of this bug, a proper deprecation cycle was unfortunately impossible. - - Generally speaking, it's advisable to prefer ``kwargs``-based initialization anyways – *especially* if you employ multiple inheritance and diamond-shaped hierarchies. - - `#298 <https://github.com/python-attrs/attrs/issues/298>`_, - `#299 <https://github.com/python-attrs/attrs/issues/299>`_, - `#304 <https://github.com/python-attrs/attrs/issues/304>`_ -- The ``__repr__`` set by ``attrs`` no longer produces an ``AttributeError`` when the instance is missing some of the specified attributes (either through deleting or after using ``init=False`` on some attributes). - - This can break code that relied on ``repr(attr_cls_instance)`` raising ``AttributeError`` to check if any ``attrs``-specified members were unset. - - If you were using this, you can implement a custom method for checking this:: - - def has_unset_members(self): - for field in attr.fields(type(self)): - try: - getattr(self, field.name) - except AttributeError: - return True - return False - - `#308 <https://github.com/python-attrs/attrs/issues/308>`_ - - -Deprecations -^^^^^^^^^^^^ - -- The ``attr.ib(convert=callable)`` option is now deprecated in favor of ``attr.ib(converter=callable)``. - - This is done to achieve consistency with other noun-based arguments like *validator*. - - *convert* will keep working until at least January 2019 while raising a ``DeprecationWarning``. - - `#307 <https://github.com/python-attrs/attrs/issues/307>`_ - - -Changes -^^^^^^^ - -- Generated ``__hash__`` methods now hash the class type along with the attribute values. - Until now the hashes of two classes with the same values were identical which was a bug. - - The generated method is also *much* faster now. - - `#261 <https://github.com/python-attrs/attrs/issues/261>`_, - `#295 <https://github.com/python-attrs/attrs/issues/295>`_, - `#296 <https://github.com/python-attrs/attrs/issues/296>`_ -- ``attr.ib``\ ’s *metadata* argument now defaults to a unique empty ``dict`` instance instead of sharing a common empty ``dict`` for all. - The singleton empty ``dict`` is still enforced. - - `#280 <https://github.com/python-attrs/attrs/issues/280>`_ -- ``ctypes`` is optional now however if it's missing, a bare ``super()`` will not work in slotted classes. - This should only happen in special environments like Google App Engine. - - `#284 <https://github.com/python-attrs/attrs/issues/284>`_, - `#286 <https://github.com/python-attrs/attrs/issues/286>`_ -- The attribute redefinition feature introduced in 17.3.0 now takes into account if an attribute is redefined via multiple inheritance. - In that case, the definition that is closer to the base of the class hierarchy wins. - - `#285 <https://github.com/python-attrs/attrs/issues/285>`_, - `#287 <https://github.com/python-attrs/attrs/issues/287>`_ -- Subclasses of ``auto_attribs=True`` can be empty now. - - `#291 <https://github.com/python-attrs/attrs/issues/291>`_, - `#292 <https://github.com/python-attrs/attrs/issues/292>`_ -- Equality tests are *much* faster now. - - `#306 <https://github.com/python-attrs/attrs/issues/306>`_ -- All generated methods now have correct ``__module__``, ``__name__``, and (on Python 3) ``__qualname__`` attributes. - - `#309 <https://github.com/python-attrs/attrs/issues/309>`_ - - ----- - - -17.3.0 (2017-11-08) -------------------- - -Backward-incompatible Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Attributes are no longer defined on the class body. - - This means that if you define a class ``C`` with an attribute ``x``, the class will *not* have an attribute ``x`` for introspection. - Instead of ``C.x``, use ``attr.fields(C).x`` or look at ``C.__attrs_attrs__``. - The old behavior has been deprecated since version 16.1. - (`#253 <https://github.com/python-attrs/attrs/issues/253>`_) - - -Changes -^^^^^^^ - -- ``super()`` and ``__class__`` now work with slotted classes on Python 3. - (`#102 <https://github.com/python-attrs/attrs/issues/102>`_, `#226 <https://github.com/python-attrs/attrs/issues/226>`_, `#269 <https://github.com/python-attrs/attrs/issues/269>`_, `#270 <https://github.com/python-attrs/attrs/issues/270>`_, `#272 <https://github.com/python-attrs/attrs/issues/272>`_) -- Added *type* argument to ``attr.ib()`` and corresponding ``type`` attribute to ``attr.Attribute``. - - This change paves the way for automatic type checking and serialization (though as of this release ``attrs`` does not make use of it). - In Python 3.6 or higher, the value of ``attr.Attribute.type`` can alternately be set using variable type annotations - (see `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_). - (`#151 <https://github.com/python-attrs/attrs/issues/151>`_, `#214 <https://github.com/python-attrs/attrs/issues/214>`_, `#215 <https://github.com/python-attrs/attrs/issues/215>`_, `#239 <https://github.com/python-attrs/attrs/issues/239>`_) -- The combination of ``str=True`` and ``slots=True`` now works on Python 2. - (`#198 <https://github.com/python-attrs/attrs/issues/198>`_) -- ``attr.Factory`` is hashable again. - (`#204 <https://github.com/python-attrs/attrs/issues/204>`_) -- Subclasses now can overwrite attribute definitions of their base classes. - - That means that you can -- for example -- change the default value for an attribute by redefining it. - (`#221 <https://github.com/python-attrs/attrs/issues/221>`_, `#229 <https://github.com/python-attrs/attrs/issues/229>`_) -- Added new option *auto_attribs* to ``@attr.s`` that allows to collect annotated fields without setting them to ``attr.ib()``. - - Setting a field to an ``attr.ib()`` is still possible to supply options like validators. - Setting it to any other value is treated like it was passed as ``attr.ib(default=value)`` -- passing an instance of ``attr.Factory`` also works as expected. - (`#262 <https://github.com/python-attrs/attrs/issues/262>`_, `#277 <https://github.com/python-attrs/attrs/issues/277>`_) -- Instances of classes created using ``attr.make_class()`` can now be pickled. - (`#282 <https://github.com/python-attrs/attrs/issues/282>`_) - - ----- - - -17.2.0 (2017-05-24) -------------------- - - -Changes: -^^^^^^^^ - -- Validators are hashable again. - Note that validators may become frozen in the future, pending availability of no-overhead frozen classes. - `#192 <https://github.com/python-attrs/attrs/issues/192>`_ - - ----- - - -17.1.0 (2017-05-16) -------------------- - -To encourage more participation, the project has also been moved into a `dedicated GitHub organization <https://github.com/python-attrs/>`_ and everyone is most welcome to join! - -``attrs`` also has a logo now! - -.. image:: https://www.attrs.org/en/latest/_static/attrs_logo.png - :alt: attrs logo - - -Backward-incompatible Changes: -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- ``attrs`` will set the ``__hash__()`` method to ``None`` by default now. - The way hashes were handled before was in conflict with `Python's specification <https://docs.python.org/3/reference/datamodel.html#object.__hash__>`_. - This *may* break some software although this breakage is most likely just surfacing of latent bugs. - You can always make ``attrs`` create the ``__hash__()`` method using ``@attr.s(hash=True)``. - See `#136`_ for the rationale of this change. - - .. warning:: - - Please *do not* upgrade blindly and *do* test your software! - *Especially* if you use instances as dict keys or put them into sets! - -- Correspondingly, ``attr.ib``'s *hash* argument is ``None`` by default too and mirrors the *cmp* argument as it should. - - -Deprecations: -^^^^^^^^^^^^^ - -- ``attr.assoc()`` is now deprecated in favor of ``attr.evolve()`` and will stop working in 2018. - - -Changes: -^^^^^^^^ - -- Fix default hashing behavior. - Now *hash* mirrors the value of *cmp* and classes are unhashable by default. - `#136`_ - `#142 <https://github.com/python-attrs/attrs/issues/142>`_ -- Added ``attr.evolve()`` that, given an instance of an ``attrs`` class and field changes as keyword arguments, will instantiate a copy of the given instance with the changes applied. - ``evolve()`` replaces ``assoc()``, which is now deprecated. - ``evolve()`` is significantly faster than ``assoc()``, and requires the class have an initializer that can take the field values as keyword arguments (like ``attrs`` itself can generate). - `#116 <https://github.com/python-attrs/attrs/issues/116>`_ - `#124 <https://github.com/python-attrs/attrs/pull/124>`_ - `#135 <https://github.com/python-attrs/attrs/pull/135>`_ -- ``FrozenInstanceError`` is now raised when trying to delete an attribute from a frozen class. - `#118 <https://github.com/python-attrs/attrs/pull/118>`_ -- Frozen-ness of classes is now inherited. - `#128 <https://github.com/python-attrs/attrs/pull/128>`_ -- ``__attrs_post_init__()`` is now run if validation is disabled. - `#130 <https://github.com/python-attrs/attrs/pull/130>`_ -- Added ``attr.validators.in_(options)`` that, given the allowed ``options``, checks whether the attribute value is in it. - This can be used to check constants, enums, mappings, etc. - `#181 <https://github.com/python-attrs/attrs/pull/181>`_ -- Added ``attr.validators.and_()`` that composes multiple validators into one. - `#161 <https://github.com/python-attrs/attrs/issues/161>`_ -- For convenience, the *validator* argument of ``@attr.s`` now can take a list of validators that are wrapped using ``and_()``. - `#138 <https://github.com/python-attrs/attrs/issues/138>`_ -- Accordingly, ``attr.validators.optional()`` now can take a list of validators too. - `#161 <https://github.com/python-attrs/attrs/issues/161>`_ -- Validators can now be defined conveniently inline by using the attribute as a decorator. - Check out the `validator examples <http://www.attrs.org/en/stable/init.html#decorator>`_ to see it in action! - `#143 <https://github.com/python-attrs/attrs/issues/143>`_ -- ``attr.Factory()`` now has a *takes_self* argument that makes the initializer to pass the partially initialized instance into the factory. - In other words you can define attribute defaults based on other attributes. - `#165`_ - `#189 <https://github.com/python-attrs/attrs/issues/189>`_ -- Default factories can now also be defined inline using decorators. - They are *always* passed the partially initialized instance. - `#165`_ -- Conversion can now be made optional using ``attr.converters.optional()``. - `#105 <https://github.com/python-attrs/attrs/issues/105>`_ - `#173 <https://github.com/python-attrs/attrs/pull/173>`_ -- ``attr.make_class()`` now accepts the keyword argument ``bases`` which allows for subclassing. - `#152 <https://github.com/python-attrs/attrs/pull/152>`_ -- Metaclasses are now preserved with ``slots=True``. - `#155 <https://github.com/python-attrs/attrs/pull/155>`_ - -.. _`#136`: https://github.com/python-attrs/attrs/issues/136 -.. _`#165`: https://github.com/python-attrs/attrs/issues/165 - - ----- - - -16.3.0 (2016-11-24) -------------------- - -Changes: -^^^^^^^^ - -- Attributes now can have user-defined metadata which greatly improves ``attrs``'s extensibility. - `#96 <https://github.com/python-attrs/attrs/pull/96>`_ -- Allow for a ``__attrs_post_init__()`` method that -- if defined -- will get called at the end of the ``attrs``-generated ``__init__()`` method. - `#111 <https://github.com/python-attrs/attrs/pull/111>`_ -- Added ``@attr.s(str=True)`` that will optionally create a ``__str__()`` method that is identical to ``__repr__()``. - This is mainly useful with ``Exception``\ s and other classes that rely on a useful ``__str__()`` implementation but overwrite the default one through a poor own one. - Default Python class behavior is to use ``__repr__()`` as ``__str__()`` anyways. - - If you tried using ``attrs`` with ``Exception``\ s and were puzzled by the tracebacks: this option is for you. -- ``__name__`` is no longer overwritten with ``__qualname__`` for ``attr.s(slots=True)`` classes. - `#99 <https://github.com/python-attrs/attrs/issues/99>`_ - - ----- - - -16.2.0 (2016-09-17) -------------------- - -Changes: -^^^^^^^^ - -- Added ``attr.astuple()`` that -- similarly to ``attr.asdict()`` -- returns the instance as a tuple. - `#77 <https://github.com/python-attrs/attrs/issues/77>`_ -- Converters now work with frozen classes. - `#76 <https://github.com/python-attrs/attrs/issues/76>`_ -- Instantiation of ``attrs`` classes with converters is now significantly faster. - `#80 <https://github.com/python-attrs/attrs/pull/80>`_ -- Pickling now works with slotted classes. - `#81 <https://github.com/python-attrs/attrs/issues/81>`_ -- ``attr.assoc()`` now works with slotted classes. - `#84 <https://github.com/python-attrs/attrs/issues/84>`_ -- The tuple returned by ``attr.fields()`` now also allows to access the ``Attribute`` instances by name. - Yes, we've subclassed ``tuple`` so you don't have to! - Therefore ``attr.fields(C).x`` is equivalent to the deprecated ``C.x`` and works with slotted classes. - `#88 <https://github.com/python-attrs/attrs/issues/88>`_ - - ----- - - -16.1.0 (2016-08-30) -------------------- - -Backward-incompatible Changes: -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- All instances where function arguments were called ``cl`` have been changed to the more Pythonic ``cls``. - Since it was always the first argument, it's doubtful anyone ever called those function with in the keyword form. - If so, sorry for any breakage but there's no practical deprecation path to solve this ugly wart. - - -Deprecations: -^^^^^^^^^^^^^ - -- Accessing ``Attribute`` instances on class objects is now deprecated and will stop working in 2017. - If you need introspection please use the ``__attrs_attrs__`` attribute or the ``attr.fields()`` function that carry them too. - In the future, the attributes that are defined on the class body and are usually overwritten in your ``__init__`` method are simply removed after ``@attr.s`` has been applied. - - This will remove the confusing error message if you write your own ``__init__`` and forget to initialize some attribute. - Instead you will get a straightforward ``AttributeError``. - In other words: decorated classes will work more like plain Python classes which was always ``attrs``'s goal. -- The serious business aliases ``attr.attributes`` and ``attr.attr`` have been deprecated in favor of ``attr.attrs`` and ``attr.attrib`` which are much more consistent and frankly obvious in hindsight. - They will be purged from documentation immediately but there are no plans to actually remove them. - - -Changes: -^^^^^^^^ - -- ``attr.asdict()``\ 's ``dict_factory`` arguments is now propagated on recursion. - `#45 <https://github.com/python-attrs/attrs/issues/45>`_ -- ``attr.asdict()``, ``attr.has()`` and ``attr.fields()`` are significantly faster. - `#48 <https://github.com/python-attrs/attrs/issues/48>`_ - `#51 <https://github.com/python-attrs/attrs/issues/51>`_ -- Add ``attr.attrs`` and ``attr.attrib`` as a more consistent aliases for ``attr.s`` and ``attr.ib``. -- Add *frozen* option to ``attr.s`` that will make instances best-effort immutable. - `#60 <https://github.com/python-attrs/attrs/issues/60>`_ -- ``attr.asdict()`` now takes ``retain_collection_types`` as an argument. - If ``True``, it does not convert attributes of type ``tuple`` or ``set`` to ``list``. - `#69 <https://github.com/python-attrs/attrs/issues/69>`_ - - ----- - - -16.0.0 (2016-05-23) -------------------- - -Backward-incompatible Changes: -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- Python 3.3 and 2.6 are no longer supported. - They may work by chance but any effort to keep them working has ceased. - - The last Python 2.6 release was on October 29, 2013 and is no longer supported by the CPython core team. - Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. - - Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. - -Changes: -^^^^^^^^ - -- ``__slots__`` have arrived! - Classes now can automatically be `slotted <https://docs.python.org/3/reference/datamodel.html#slots>`_-style (and save your precious memory) just by passing ``slots=True``. - `#35 <https://github.com/python-attrs/attrs/issues/35>`_ -- Allow the case of initializing attributes that are set to ``init=False``. - This allows for clean initializer parameter lists while being able to initialize attributes to default values. - `#32 <https://github.com/python-attrs/attrs/issues/32>`_ -- ``attr.asdict()`` can now produce arbitrary mappings instead of Python ``dict``\ s when provided with a ``dict_factory`` argument. - `#40 <https://github.com/python-attrs/attrs/issues/40>`_ -- Multiple performance improvements. - - ----- - - -15.2.0 (2015-12-08) -------------------- - -Changes: -^^^^^^^^ - -- Added a ``convert`` argument to ``attr.ib``, which allows specifying a function to run on arguments. - This allows for simple type conversions, e.g. with ``attr.ib(convert=int)``. - `#26 <https://github.com/python-attrs/attrs/issues/26>`_ -- Speed up object creation when attribute validators are used. - `#28 <https://github.com/python-attrs/attrs/issues/28>`_ - - ----- - - -15.1.0 (2015-08-20) -------------------- - -Changes: -^^^^^^^^ - -- Added ``attr.validators.optional()`` that wraps other validators allowing attributes to be ``None``. - `#16 <https://github.com/python-attrs/attrs/issues/16>`_ -- Multi-level inheritance now works. - `#24 <https://github.com/python-attrs/attrs/issues/24>`_ -- ``__repr__()`` now works with non-redecorated subclasses. - `#20 <https://github.com/python-attrs/attrs/issues/20>`_ - - ----- - - -15.0.0 (2015-04-15) -------------------- - -Changes: -^^^^^^^^ - -Initial release. diff --git a/tests/wpt/tests/tools/third_party/attrs/CITATION.cff b/tests/wpt/tests/tools/third_party/attrs/CITATION.cff new file mode 100644 index 00000000000..83718ad889d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/CITATION.cff @@ -0,0 +1,9 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it as below. +title: attrs +type: software +authors: + - given-names: Hynek + family-names: Schlawack + email: hs@ox.cx +doi: 10.5281/zenodo.6925130 diff --git a/tests/wpt/tests/tools/third_party/attrs/LICENSE b/tests/wpt/tests/tools/third_party/attrs/LICENSE index 7ae3df93097..2bd6453d255 100644 --- a/tests/wpt/tests/tools/third_party/attrs/LICENSE +++ b/tests/wpt/tests/tools/third_party/attrs/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Hynek Schlawack +Copyright (c) 2015 Hynek Schlawack and the attrs contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/tests/wpt/tests/tools/third_party/attrs/MANIFEST.in b/tests/wpt/tests/tools/third_party/attrs/MANIFEST.in deleted file mode 100644 index 3d68bf9c5d5..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/MANIFEST.in +++ /dev/null @@ -1,24 +0,0 @@ -include LICENSE *.rst *.toml *.yml *.yaml *.ini -graft .github - -# Stubs -recursive-include src *.pyi -recursive-include src py.typed - -# Tests -include tox.ini conftest.py -recursive-include tests *.py -recursive-include tests *.yml - -# Documentation -include docs/Makefile docs/docutils.conf -recursive-include docs *.png -recursive-include docs *.svg -recursive-include docs *.py -recursive-include docs *.rst -prune docs/_build - -# Just to keep check-manifest happy; on releases those files are gone. -# Last rule wins! -exclude changelog.d/*.rst -include changelog.d/towncrier_template.rst diff --git a/tests/wpt/tests/tools/third_party/attrs/README.md b/tests/wpt/tests/tools/third_party/attrs/README.md new file mode 100644 index 00000000000..6ef8c0204ea --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/README.md @@ -0,0 +1,133 @@ +<p align="center"> + <a href="https://www.attrs.org/"> + <picture> + <source srcset="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo_white.svg" media="(prefers-color-scheme: dark)"> + <img src="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo.svg" width="35%" alt="attrs" /> + </picture> + </a> +</p> + +<p align="center"> + <a href="https://www.attrs.org/en/stable/"><img src="https://img.shields.io/badge/Docs-RTD-black" alt="Documentation" /></a> + <a href="https://github.com/python-attrs/attrs/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-C06524" alt="License: MIT" /></a> + <a href="https://bestpractices.coreinfrastructure.org/projects/6482"><img src="https://bestpractices.coreinfrastructure.org/projects/6482/badge"></a> + <a href="https://pypi.org/project/attrs/"><img src="https://img.shields.io/pypi/v/attrs" /></a> + <a href="https://pepy.tech/project/attrs"><img src="https://static.pepy.tech/personalized-badge/attrs?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Downloads%20/%20Month" alt="Downloads per month" /></a> + <a href="https://zenodo.org/badge/latestdoi/29918975"><img src="https://zenodo.org/badge/29918975.svg" alt="DOI"></a> +</p> + +<!-- teaser-begin --> + +*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)). +[Trusted by NASA](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-achievement) for Mars missions since 2020! + +Its main goal is to help you to write **concise** and **correct** software without slowing down your code. + + +## Sponsors + +*attrs* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek). +Especially those generously supporting us at the *The Organization* tier and higher: + +<p align="center"> + <a href="https://www.variomedia.de/"><img src="https://www.attrs.org/en/latest/_static/sponsors/Variomedia.svg" width="200" height="60" /></a> + <a href="https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo"><img src="https://www.attrs.org/en/latest/_static/sponsors/Tidelift.svg" width="200" height="60" /></a> + <a href="https://filepreviews.io/"><img src="https://www.attrs.org/en/latest/_static/sponsors/FilePreviews.svg" width="200" height="60"/></a> +</p> + +<p align="center"> + <strong>Please consider <a href="https://github.com/sponsors/hynek">joining them</a> to help make <em>attrs</em>’s maintenance more sustainable!</strong> +</p> + +<!-- teaser-end --> + +## Example + +*attrs* gives you a class decorator and a way to declaratively define the attributes on that class: + +<!-- code-begin --> + +```pycon +>>> from attrs import asdict, define, make_class, Factory + +>>> @define +... class SomeClass: +... a_number: int = 42 +... list_of_numbers: list[int] = Factory(list) +... +... def hard_math(self, another_number): +... return self.a_number + sum(self.list_of_numbers) * another_number + + +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) + +>>> sc.hard_math(3) +19 +>>> sc == SomeClass(1, [1, 2, 3]) +True +>>> sc != SomeClass(2, [3, 2, 1]) +True + +>>> asdict(sc) +{'a_number': 1, 'list_of_numbers': [1, 2, 3]} + +>>> SomeClass() +SomeClass(a_number=42, list_of_numbers=[]) + +>>> C = make_class("C", ["a", "b"]) +>>> C("foo", "bar") +C(a='foo', b='bar') +``` + +After *declaring* your attributes, *attrs* gives you: + +- a concise and explicit overview of the class's attributes, +- a nice human-readable `__repr__`, +- equality-checking methods, +- an initializer, +- and much more, + +*without* writing dull boilerplate code again and again and *without* runtime performance penalties. + +**Hate type annotations**!? +No problem! +Types are entirely **optional** with *attrs*. +Simply assign `attrs.field()` to the attributes instead of annotating them with types. + +--- + +This example uses *attrs*'s modern APIs that have been introduced in version 20.1.0, and the *attrs* package import name that has been added in version 21.3.0. +The classic APIs (`@attr.s`, `attr.ib`, plus their serious-business aliases) and the `attr` package import name will remain **indefinitely**. + +Please check out [*On The Core API Names*](https://www.attrs.org/en/latest/names.html) for a more in-depth explanation. + + +## Data Classes + +On the tin, *attrs* might remind you of `dataclasses` (and indeed, `dataclasses` [are a descendant](https://hynek.me/articles/import-attrs/) of *attrs*). +In practice it does a lot more and is more flexible. +For instance it allows you to define [special handling of NumPy arrays for equality checks](https://www.attrs.org/en/stable/comparison.html#customization), allows more ways to [plug into the initialization process](https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization), and allows for stepping through the generated methods using a debugger. + +For more details, please refer to our [comparison page](https://www.attrs.org/en/stable/why.html#data-classes). + + +## Project Information + +- [**Changelog**](https://www.attrs.org/en/stable/changelog.html) +- [**Documentation**](https://www.attrs.org/) +- [**PyPI**](https://pypi.org/project/attrs/) +- [**Source Code**](https://github.com/python-attrs/attrs) +- [**Contributing**](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) +- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs) +- **Get Help**: please use the `python-attrs` tag on [StackOverflow](https://stackoverflow.com/questions/tagged/python-attrs) + + +### *attrs* for Enterprise + +Available as part of the Tidelift Subscription. + +The maintainers of *attrs* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. +Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. +[Learn more.](https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/tests/wpt/tests/tools/third_party/attrs/README.rst b/tests/wpt/tests/tools/third_party/attrs/README.rst deleted file mode 100644 index 709bba83d7e..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/README.rst +++ /dev/null @@ -1,135 +0,0 @@ -.. raw:: html - - <p align="center"> - <a href="https://www.attrs.org/"> - <img src="./docs/_static/attrs_logo.svg" width="35%" alt="attrs" /> - </a> - </p> - <p align="center"> - <a href="https://www.attrs.org/en/stable/?badge=stable"> - <img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation" /> - </a> - <a href="https://github.com/python-attrs/attrs/blob/main/LICENSE"> - <img src="https://img.shields.io/badge/license-MIT-C06524" alt="License: MIT" /> - </a> - <a href="https://pypi.org/project/attrs/"> - <img src="https://img.shields.io/pypi/v/attrs" /> - </a> - </p> - -.. teaser-begin - -``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder methods <https://www.attrs.org/en/latest/glossary.html#term-dunder-methods>`_). -`Trusted by NASA <https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-badge>`_ for Mars missions since 2020! - -Its main goal is to help you to write **concise** and **correct** software without slowing down your code. - -.. teaser-end - -For that, it gives you a class decorator and a way to declaratively define the attributes on that class: - -.. -code-begin- - -.. code-block:: pycon - - >>> from attrs import asdict, define, make_class, Factory - - >>> @define - ... class SomeClass: - ... a_number: int = 42 - ... list_of_numbers: list[int] = Factory(list) - ... - ... def hard_math(self, another_number): - ... return self.a_number + sum(self.list_of_numbers) * another_number - - - >>> sc = SomeClass(1, [1, 2, 3]) - >>> sc - SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) - - >>> sc.hard_math(3) - 19 - >>> sc == SomeClass(1, [1, 2, 3]) - True - >>> sc != SomeClass(2, [3, 2, 1]) - True - - >>> asdict(sc) - {'a_number': 1, 'list_of_numbers': [1, 2, 3]} - - >>> SomeClass() - SomeClass(a_number=42, list_of_numbers=[]) - - >>> C = make_class("C", ["a", "b"]) - >>> C("foo", "bar") - C(a='foo', b='bar') - - -After *declaring* your attributes ``attrs`` gives you: - -- a concise and explicit overview of the class's attributes, -- a nice human-readable ``__repr__``, -- a equality-checking methods, -- an initializer, -- and much more, - -*without* writing dull boilerplate code again and again and *without* runtime performance penalties. - -**Hate type annotations**!? -No problem! -Types are entirely **optional** with ``attrs``. -Simply assign ``attrs.field()`` to the attributes instead of annotating them with types. - ----- - -This example uses ``attrs``'s modern APIs that have been introduced in version 20.1.0, and the ``attrs`` package import name that has been added in version 21.3.0. -The classic APIs (``@attr.s``, ``attr.ib``, plus their serious business aliases) and the ``attr`` package import name will remain **indefinitely**. - -Please check out `On The Core API Names <https://www.attrs.org/en/latest/names.html>`_ for a more in-depth explanation. - - -Data Classes -============ - -On the tin, ``attrs`` might remind you of ``dataclasses`` (and indeed, ``dataclasses`` are a descendant of ``attrs``). -In practice it does a lot more and is more flexible. -For instance it allows you to define `special handling of NumPy arrays for equality checks <https://www.attrs.org/en/stable/comparison.html#customization>`_, or allows more ways to `plug into the initialization process <https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization>`_. - -For more details, please refer to our `comparison page <https://www.attrs.org/en/stable/why.html#data-classes>`_. - - -.. -getting-help- - -Getting Help -============ - -Please use the ``python-attrs`` tag on `Stack Overflow <https://stackoverflow.com/questions/tagged/python-attrs>`_ to get help. - -Answering questions of your fellow developers is also a great way to help the project! - - -.. -project-information- - -Project Information -=================== - -``attrs`` is released under the `MIT <https://choosealicense.com/licenses/mit/>`_ license, -its documentation lives at `Read the Docs <https://www.attrs.org/>`_, -the code on `GitHub <https://github.com/python-attrs/attrs>`_, -and the latest release on `PyPI <https://pypi.org/project/attrs/>`_. -It’s rigorously tested on Python 2.7, 3.5+, and PyPy. - -We collect information on **third-party extensions** in our `wiki <https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs>`_. -Feel free to browse and add your own! - -If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide <https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md>`_ to get you started! - - -``attrs`` for Enterprise ------------------------- - -Available as part of the Tidelift Subscription. - -The maintainers of ``attrs`` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. -Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. -`Learn more. <https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo>`_ diff --git a/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.md.jinja b/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.md.jinja new file mode 100644 index 00000000000..d9ae7c10efc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.md.jinja @@ -0,0 +1,28 @@ +{%- if versiondata["version"] == "main" -%} +## Changes for the Upcoming Release + +:::{warning} +These changes reflect the current [development progress](https://github.com/python-attrs/attrs/tree/main) and have **not** been part of a PyPI release yet. +::: +{% else -%} +## [{{ versiondata["version"] }}](https://github.com/python-attrs/attrs/tree/{{ versiondata["version"] }}) - {{ versiondata["date"] }} +{%- endif %} + +{% for section, _ in sections.items() %} +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} + +### {{ definitions[category]['name'] }} + +{% for text, values in sections[section][category].items() %} +- {{ text }} + {{ values|join(',\n ') }} +{% endfor %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} diff --git a/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst b/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst deleted file mode 100644 index 29ca74c4e8f..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/changelog.d/towncrier_template.rst +++ /dev/null @@ -1,35 +0,0 @@ -{% for section, _ in sections.items() %} -{% set underline = underlines[0] %}{% if section %}{{section}} -{{ underline * section|length }}{% set underline = underlines[1] %} - -{% endif %} - -{% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section]%} -{{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} - -{% if definitions[category]['showcontent'] %} -{% for text, values in sections[section][category].items() %} -- {{ text }} - {{ values|join(',\n ') }} -{% endfor %} - -{% else %} -- {{ sections[section][category]['']|join(', ') }} - -{% endif %} -{% if sections[section][category]|length == 0 %} -No significant changes. - -{% else %} -{% endif %} - -{% endfor %} -{% else %} -No significant changes. - - -{% endif %} -{% endfor %} ----- diff --git a/tests/wpt/tests/tools/third_party/attrs/conftest.py b/tests/wpt/tests/tools/third_party/attrs/conftest.py index 0d539a115c7..144e5f3e19f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/conftest.py +++ b/tests/wpt/tests/tools/third_party/attrs/conftest.py @@ -1,10 +1,20 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function +import pytest from hypothesis import HealthCheck, settings -from attr._compat import PY36, PY310 +from attr._compat import PY310 + + +@pytest.fixture(name="slots", params=(True, False)) +def _slots(request): + return request.param + + +@pytest.fixture(name="frozen", params=(True, False)) +def _frozen(request): + return request.param def pytest_configure(config): @@ -16,14 +26,5 @@ def pytest_configure(config): collect_ignore = [] -if not PY36: - collect_ignore.extend( - [ - "tests/test_annotations.py", - "tests/test_hooks.py", - "tests/test_init_subclass.py", - "tests/test_next_gen.py", - ] - ) if not PY310: collect_ignore.extend(["tests/test_pattern_matching.py"]) diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/custom.css b/tests/wpt/tests/tools/third_party/attrs/docs/_static/custom.css new file mode 100644 index 00000000000..72083fee482 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/custom.css @@ -0,0 +1,10 @@ +@import url('https://rsms.me/inter/inter.css'); +@import url('https://assets.hynek.me/css/bm.css'); + + +:root { + font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */ +} +@supports (font-variation-settings: normal) { + :root { font-family: InterVariable, sans-serif; } +} diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon.png b/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon.png Binary files differnew file mode 100644 index 00000000000..d9886f1f6f6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon.png diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon@2x.png b/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon@2x.png Binary files differnew file mode 100644 index 00000000000..5551b42bd92 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/docset-icon@2x.png diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.afdesign b/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.afdesign Binary files differnew file mode 100644 index 00000000000..06fa6696059 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.afdesign diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.png b/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.png Binary files differnew file mode 100644 index 00000000000..46af5dcfd69 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/social card.png diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/FilePreviews.svg b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/FilePreviews.svg new file mode 100644 index 00000000000..2fff3aa3aea --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/FilePreviews.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 180 60" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path fill="#fff" d="M0 0h180v60H0z"/><path fill="#fff" d="M0 0h180v60H0z"/><path d="M27.917 35.296v9.608h-5V15.662h20v5.013h-15v9.608h8.75v5.013h-8.75zM32.5 54.93c13.807 0 25-11.222 25-25.065C57.5 16.022 46.307 4.8 32.5 4.8s-25 11.222-25 25.065c0 13.843 11.193 25.065 25 25.065zm5.417-33.002v13.368h5V21.928h-5zm30.694.302h8.95v2.16h-6.483v3.81h6.11v2.08h-6.11v5.851h-2.467v-13.9zm10.165 4.477h2.35v9.424h-2.35v-9.424zm-.333-3.2c0-.38.14-.71.421-.992.28-.282.637-.422 1.068-.422.43 0 .793.134 1.086.402.294.268.441.606.441 1.012 0 .405-.146.742-.44 1.01-.295.269-.656.403-1.087.403-.431 0-.787-.14-1.068-.422a1.358 1.358 0 01-.42-.991zm4.957-2.22h2.35v14.844H83.4V21.288zm11.203 9.19c0-.301-.042-.59-.127-.864a1.97 1.97 0 00-.402-.727 1.96 1.96 0 00-.695-.5 2.422 2.422 0 00-.989-.187c-.705 0-1.302.213-1.792.638-.49.426-.76.972-.812 1.64h4.817zm2.35 1.06v.314c0 .104-.007.21-.02.314h-7.147a2.336 2.336 0 00.861 1.66c.248.202.529.363.842.48.314.118.64.177.98.177.587 0 1.083-.108 1.488-.324.405-.217.737-.514.998-.893l1.567 1.256c-.927 1.257-2.272 1.885-4.034 1.885a5.715 5.715 0 01-2.017-.344 4.75 4.75 0 01-1.596-.971 4.39 4.39 0 01-1.058-1.542c-.255-.609-.381-1.299-.381-2.07 0-.76.127-1.45.381-2.072a4.713 4.713 0 011.048-1.59c.444-.439.97-.78 1.576-1.022a5.272 5.272 0 011.969-.363c.653 0 1.256.108 1.811.324a3.93 3.93 0 011.44.962c.404.425.72.956.95 1.59.228.635.342 1.378.342 2.229zm1.764-9.307h4.524a9.82 9.82 0 012.154.216c.64.144 1.188.38 1.645.707.457.327.81.746 1.057 1.257.249.51.373 1.125.373 1.845 0 .773-.144 1.42-.431 1.944a3.498 3.498 0 01-1.165 1.266c-.49.32-1.061.553-1.714.698a9.62 9.62 0 01-2.076.216h-1.9v5.752h-2.467v-13.9zm4.191 6.067c.404 0 .79-.029 1.155-.088.366-.059.692-.164.98-.314.287-.151.515-.357.685-.619.17-.262.254-.602.254-1.02 0-.406-.084-.737-.254-.992a1.761 1.761 0 00-.676-.6c-.28-.143-.6-.238-.96-.284a8.718 8.718 0 00-1.106-.069h-1.801v3.986h1.723zm6.112-1.59h2.252v1.57h.039a3.43 3.43 0 011.097-1.335c.47-.34 1.018-.51 1.645-.51.091 0 .189.003.293.01.105.007.196.023.274.049v2.16a2.728 2.728 0 00-.724-.098c-.536 0-.966.098-1.292.294a2.344 2.344 0 00-.764.707 2.534 2.534 0 00-.372.844 3.289 3.289 0 00-.098.687v5.046h-2.35v-9.424zm12.77 3.77c0-.301-.043-.59-.127-.864a1.97 1.97 0 00-.402-.727 1.96 1.96 0 00-.696-.5 2.42 2.42 0 00-.988-.187c-.705 0-1.302.213-1.792.638-.49.426-.76.972-.813 1.64h4.818zm2.35 1.06v.314c0 .104-.007.21-.02.314h-7.148a2.336 2.336 0 00.862 1.66c.248.202.529.363.842.48.313.118.64.177.979.177.588 0 1.083-.108 1.488-.324a2.813 2.813 0 001-.893l1.566 1.256c-.927 1.257-2.272 1.885-4.034 1.885a5.716 5.716 0 01-2.017-.344 4.748 4.748 0 01-1.596-.971 4.39 4.39 0 01-1.058-1.542c-.255-.609-.382-1.299-.382-2.07 0-.76.127-1.45.382-2.072a4.718 4.718 0 011.048-1.59c.443-.439.969-.78 1.576-1.022a5.272 5.272 0 011.968-.363c.653 0 1.257.108 1.812.324a3.93 3.93 0 011.44.962c.404.425.72.956.95 1.59.228.635.342 1.378.342 2.229zm.236-4.83h2.566l2.467 6.872h.04l2.486-6.872h2.487l-3.74 9.424h-2.506l-3.8-9.424zm10.87 0h2.35v9.424h-2.35v-9.424zm-.332-3.2c0-.38.14-.71.42-.992.281-.282.637-.422 1.068-.422.431 0 .793.134 1.087.402.293.268.44.606.44 1.012 0 .405-.146.742-.44 1.01-.294.269-.656.403-1.087.403-.43 0-.786-.14-1.067-.422a1.358 1.358 0 01-.421-.991zm11.536 6.97c0-.301-.043-.59-.127-.864a1.973 1.973 0 00-.402-.727 1.958 1.958 0 00-.695-.5 2.42 2.42 0 00-.989-.187c-.705 0-1.302.213-1.792.638-.49.426-.76.972-.812 1.64h4.817zm2.35 1.06v.314c0 .104-.007.21-.02.314h-7.147a2.336 2.336 0 00.861 1.66c.248.202.529.363.842.48.314.118.64.177.98.177.587 0 1.083-.108 1.488-.324a2.81 2.81 0 00.999-.893l1.566 1.256c-.927 1.257-2.271 1.885-4.034 1.885a5.717 5.717 0 01-2.017-.344 4.751 4.751 0 01-1.596-.971 4.385 4.385 0 01-1.057-1.542c-.255-.609-.383-1.299-.383-2.07 0-.76.128-1.45.382-2.072a4.709 4.709 0 011.048-1.59c.444-.439.97-.78 1.577-1.022a5.271 5.271 0 011.968-.363c.653 0 1.256.108 1.811.324a3.926 3.926 0 011.44.962c.404.425.72.956.95 1.59.228.635.342 1.378.342 2.229zm.315-4.83h2.546l1.958 6.754h.04l2.016-6.754h2.507l2.037 6.754h.039l1.998-6.754h2.448l-3.252 9.424h-2.388l-2.135-6.597h-.04l-2.114 6.597h-2.39l-3.27-9.424zm21.602 2.513a2.709 2.709 0 00-.862-.717 2.42 2.42 0 00-1.194-.304c-.379 0-.725.079-1.038.236-.313.157-.47.419-.47.785 0 .367.173.625.52.776.346.15.85.304 1.517.461.352.078.708.184 1.067.314.36.131.685.304.98.52.294.217.531.485.714.806.183.32.274.71.274 1.168 0 .576-.107 1.063-.323 1.463a2.757 2.757 0 01-.861.971 3.612 3.612 0 01-1.254.54c-.476.111-.97.167-1.479.167a5.853 5.853 0 01-2.134-.402c-.692-.27-1.267-.651-1.724-1.149l1.548-1.453c.26.34.6.622 1.018.844a2.91 2.91 0 001.39.334c.17 0 .343-.02.52-.059.176-.039.339-.101.49-.186.15-.085.27-.2.361-.344a.972.972 0 00.137-.53c0-.393-.179-.674-.538-.844-.36-.17-.898-.34-1.616-.51a7.739 7.739 0 01-1.028-.305 3.363 3.363 0 01-.891-.49 2.275 2.275 0 01-.627-.757c-.156-.3-.235-.674-.235-1.119 0-.524.108-.975.324-1.355.215-.38.499-.69.851-.932a3.814 3.814 0 011.195-.54c.444-.118.9-.177 1.37-.177.68 0 1.342.118 1.988.353.646.236 1.16.596 1.538 1.08l-1.528 1.355z" fill="#1083ff"/></svg> diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Tidelift.svg b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Tidelift.svg new file mode 100644 index 00000000000..8b4da42cb15 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Tidelift.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 180 60" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path fill="#fff" d="M0 0h180v60H0z"/><g fill-rule="nonzero"><path d="M39.99 39.07V20.93c0-1.838 0-1.838 1.937-1.838s1.937 0 1.937 1.837v18.142c0 1.837 0 1.837-1.937 1.837s-1.937 0-1.937-1.837zm9.422-1.04V21.97c0-2.883 0-2.883 2.883-2.883h4.699c6.294 0 10.168 3.142 10.58 8.259a25.26 25.26 0 010 4.483c-.416 5.084-4.29 9.08-10.58 9.08h-4.699c-2.883 0-2.883 0-2.883-2.88zm7.498-.797c4.308 0 6.524-2.433 6.774-5.779a20.04 20.04 0 000-3.72c-.284-3.38-2.466-4.963-6.774-4.963h-3.62v14.462h3.62zm15.855.797V21.97c0-2.883 0-2.883 2.882-2.883h10.914c1.904 0 1.904 0 1.904 1.788 0 1.787 0 1.82-1.904 1.82h-9.914v5.617h8.96c1.82 0 1.82 0 1.82 1.705 0 1.704 0 1.72-1.82 1.72h-8.96V37.3h9.998c1.903 0 1.903 0 1.903 1.77 0 1.838 0 1.838-1.903 1.838H75.647c-2.882 0-2.882 0-2.882-2.879zm21.261 0V20.945c0-1.871 0-1.871 1.92-1.871s1.937 0 1.937 1.837V37.3h9.64c1.953 0 1.953 0 1.953 1.82 0 1.822 0 1.788-1.953 1.788H96.909c-2.883 0-2.883 0-2.883-2.879zm20.944 1.04V20.93c0-1.838 0-1.838 1.938-1.838 1.937 0 1.937 0 1.937 1.837v18.142c0 1.837 0 1.837-1.937 1.837s-1.937 0-1.937-1.837zm9.378 0V21.988c0-2.883 0-2.883 2.882-2.883h10.914c1.87 0 1.887 0 1.887 1.821 0 1.82 0 1.787-1.887 1.787h-9.918v5.834h9.193c1.887 0 1.887 0 1.887 1.754s0 1.788-1.887 1.788h-9.193v6.983c0 1.837 0 1.837-1.937 1.837s-1.941 0-1.941-1.837zM32.533 19.092H17.346c-1.987 0-1.987 0-1.987 1.82 0 1.821 0 1.788 1.987 1.788h5.665v6.437l3.857-3.025V22.7h5.665c1.954 0 1.954 0 1.954-1.788 0-1.787 0-1.82-1.954-1.82z" fill="#4b5168"/><path d="M23.01 32.87v6.15c0 1.838 0 1.838 1.938 1.838 1.937 0 1.92 0 1.92-1.87v-9.142l-3.857 3.025z" fill="#f6914d"/><path d="M147.463 19.092h15.187c1.987 0 1.987 0 1.987 1.82 0 1.821 0 1.788-1.987 1.788h-5.665v6.437l-3.857-3.025V22.7h-5.665c-1.954 0-1.954 0-1.954-1.788 0-1.787.004-1.82 1.954-1.82z" fill="#4b5168"/><path d="M156.99 32.87v6.15c0 1.838 0 1.838-1.938 1.838-1.937 0-1.92 0-1.92-1.87v-9.142l3.857 3.025z" fill="#f6914d"/></g></svg> diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Variomedia.svg b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Variomedia.svg new file mode 100644 index 00000000000..90e750d2574 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/_static/sponsors/Variomedia.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 180 60" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path fill="#fff" d="M0 0h180v60H0z"/><g fill-rule="nonzero"><path d="M39.98 2.528c-.986 0-1.777.796-1.777 1.781 0 .324.087.627.238.888l18.6 38.156 6.966-17.195L53.047 3.61a1.783 1.783 0 00-1.566-1.08H39.979z" fill="#3386aa"/><path d="M74.781 2.53c-.77 0-1.424.482-1.674 1.166L57.039 43.354l6.33 13.01c.294.656.905 1.107 1.65 1.107.73 0 1.366-.38 1.716-1.177l20.67-51.241c.094-.23.166-.479.166-.745 0-.986-.797-1.772-1.781-1.779H74.78z" fill="#2ac2ba"/><path d="M89.969 55.009a1.795 1.795 0 001.656 2.463l11.005-.002c.742 0 1.379-.458 1.647-1.102l.003-.002 20.718-51.39a1.782 1.782 0 00-1.652-2.447h-11.011a1.77 1.77 0 00-1.665 1.148L89.969 55.009z" fill="#ec1e79"/><path d="M64.006 26.16l-1.763-3.629-5.204 20.823.63-1.554 6.337-15.64z" fill="#276680"/><path d="M89.873 56.02v-.002l-.009-.05v-.002a1.844 1.844 0 01-.022-.225v-.008c0-.018 0-.035-.002-.053v-.008c0-.236.047-.458.13-.664 1.946-4.833 7.794-19.328 7.794-19.328l.07-.175L87.52 3.88l.01.044c.002.015.006.03.009.044v.001l.008.044v.001l.006.044v.001l.006.045v.001l.005.045v.002l.002.045c.001.024.003.048.003.073v.065c0 .047-.004.094-.01.141 0 .016-.003.03-.005.046-.001.016-.004.03-.007.046l-.008.045v.002l-.01.043v.002l-.01.042v.003a.29.29 0 01-.011.04l-.001.006a.303.303 0 01-.011.039l-.002.007-.011.037-.003.009c-.004.01-.006.022-.01.034l-.005.013a.2.2 0 01-.011.03l-.006.016-.01.027-.008.022a.58.58 0 01-.025.061L79.586 24.43l10.322 31.74a2.272 2.272 0 01-.035-.15zm51.863-.782L125.07 3.863a1.776 1.776 0 01-.072 1.113L118.6 20.85l-1.445 3.584 10.333 31.786a1.79 1.79 0 001.701 1.25h10.838a1.777 1.777 0 001.71-2.233z" fill="#b1175b"/><path d="M118.86 29.685l-1.706-5.25 7.828-19.417-6.123 24.667zm-37.565 0l-1.707-5.25L87.42 5.02l-6.125 24.666zm14.83.581l1.709 5.24-7.831 19.417 6.122-24.657z" fill="#841245"/></g></svg> diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/api-attr.rst b/tests/wpt/tests/tools/third_party/attrs/docs/api-attr.rst new file mode 100644 index 00000000000..1c1c3edb3fc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/api-attr.rst @@ -0,0 +1,242 @@ +API Reference for the ``attr`` Namespace +======================================== + +.. module:: attr + + +Core +---- + +.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True, unsafe_hash=None) + + .. note:: + + *attrs* also comes with a serious-business alias ``attr.attrs``. + + For example: + + .. doctest:: + + >>> import attr + >>> @attr.s + ... class C: + ... _private = attr.ib() + >>> C(private=42) + C(_private=42) + >>> class D: + ... def __init__(self, x): + ... self.x = x + >>> D(1) + <D object at ...> + >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) + >>> D(1) + D(x=1) + >>> @attr.s(auto_exc=True) + ... class Error(Exception): + ... x = attr.ib() + ... y = attr.ib(default=42, init=False) + >>> Error("foo") + Error(x='foo', y=42) + >>> raise Error("foo") + Traceback (most recent call last): + ... + Error: ('foo', 42) + >>> raise ValueError("foo", 42) # for comparison + Traceback (most recent call last): + ... + ValueError: ('foo', 42) + + +.. autofunction:: attr.ib + + .. note:: + + *attrs* also comes with a serious-business alias ``attr.attrib``. + + The object returned by `attr.ib` also allows for setting the default and the validator using decorators: + + .. doctest:: + + >>> @attr.s + ... class C: + ... x = attr.ib() + ... y = attr.ib() + ... @x.validator + ... def _any_name_except_a_name_of_an_attribute(self, attribute, value): + ... if value < 0: + ... raise ValueError("x must be positive") + ... @y.default + ... def _any_name_except_a_name_of_an_attribute(self): + ... return self.x + 1 + >>> C(1) + C(x=1, y=2) + >>> C(-1) + Traceback (most recent call last): + ... + ValueError: x must be positive + + +.. function:: define + + Same as `attrs.define`. + +.. function:: mutable + + Same as `attrs.mutable`. + +.. function:: frozen + + Same as `attrs.frozen`. + +.. function:: field + + Same as `attrs.field`. + +.. class:: Attribute + + Same as `attrs.Attribute`. + +.. function:: make_class + + Same as `attrs.make_class`. + +.. autoclass:: Factory + :noindex: + + Same as `attrs.Factory`. + + +.. data:: NOTHING + + Same as `attrs.NOTHING`. + + +Exceptions +---------- + +.. module:: attr.exceptions + +All exceptions are available from both ``attr.exceptions`` and `attrs.exceptions` (it's the same module in a different namespace). + +Please refer to `attrs.exceptions` for details. + + +Helpers +------- + +.. currentmodule:: attr + +.. function:: cmp_using + + Same as `attrs.cmp_using`. + +.. function:: fields + + Same as `attrs.fields`. + +.. function:: fields_dict + + Same as `attr.fields_dict`. + +.. function:: has + + Same as `attrs.has`. + +.. function:: resolve_types + + Same as `attrs.resolve_types`. + +.. autofunction:: asdict +.. autofunction:: astuple + +.. module:: attr.filters + +.. function:: include + + Same as `attrs.filters.include`. + +.. function:: exclude + + Same as `attrs.filters.exclude`. + +See :func:`attrs.asdict` for examples. + +All objects from `attrs.filters` are also available in ``attr.filters``. + +---- + +.. currentmodule:: attr + +.. function:: evolve + + Same as `attrs.evolve`. + +.. function:: validate + + Same as `attrs.validate`. + + +Validators +---------- + +.. module:: attr.validators + +All objects from `attrs.validators` are also available in ``attr.validators``. +Please refer to the former for details. + + +Converters +---------- + +.. module:: attr.converters + +All objects from `attrs.converters` are also available from ``attr.converters``. +Please refer to the former for details. + + +Setters +------- + +.. module:: attr.setters + +All objects from `attrs.setters` are also available in ``attr.setters``. +Please refer to the former for details. + + +Deprecated APIs +--------------- + +.. currentmodule:: attr + +To help you write backward compatible code that doesn't throw warnings on modern releases, the ``attr`` module has an ``__version_info__`` attribute as of version 19.2.0. +It behaves similarly to `sys.version_info` and is an instance of `attr.VersionInfo`: + +.. autoclass:: VersionInfo + + With its help you can write code like this: + + >>> if getattr(attr, "__version_info__", (0,)) >= (19, 2): + ... cmp_off = {"eq": False} + ... else: + ... cmp_off = {"cmp": False} + >>> cmp_off == {"eq": False} + True + >>> @attr.s(**cmp_off) + ... class C: + ... pass + + +---- + +.. autofunction:: assoc + +Before *attrs* got `attrs.validators.set_disabled` and `attrs.validators.set_disabled`, it had the following APIs to globally enable and disable validators. +They won't be removed, but are discouraged to use: + +.. autofunction:: set_run_validators +.. autofunction:: get_run_validators + +---- + +The serious-business aliases used to be called ``attr.attributes`` and ``attr.attr``. +There are no plans to remove them but they shouldn't be used in new code. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/api.rst b/tests/wpt/tests/tools/third_party/attrs/docs/api.rst index 02aed52ad5d..d55f2539ea5 100644 --- a/tests/wpt/tests/tools/third_party/attrs/docs/api.rst +++ b/tests/wpt/tests/tools/third_party/attrs/docs/api.rst @@ -1,83 +1,62 @@ API Reference ============= -.. currentmodule:: attr +.. module:: attrs -``attrs`` works by decorating a class using `attrs.define` or `attr.s` and then optionally defining attributes on the class using `attrs.field`, `attr.ib`, or a type annotation. +*attrs* works by decorating a class using `attrs.define` or `attr.s` and then defining attributes on the class using `attrs.field`, `attr.ib`, or type annotations. -If you're confused by the many names, please check out `names` for clarification. +What follows is the API explanation, if you'd like a more hands-on tutorial, have a look at `examples`. -What follows is the API explanation, if you'd like a more hands-on introduction, have a look at `examples`. +If you're confused by the many names, please check out `names` for clarification, but the `TL;DR <https://en.wikipedia.org/wiki/TL;DR>`_ is that as of version 21.3.0, *attrs* consists of **two** top-level package names: -As of version 21.3.0, ``attrs`` consists of **two** to-level package names: - -- The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` -- The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. +- The classic ``attr`` that powers the venerable `attr.s` and `attr.ib`. +- The newer ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). - Using this namespace requires Python 3.6 or later. -The ``attrs`` namespace is built *on top of* ``attr`` which will *never* go away. +The ``attrs`` namespace is built *on top of* ``attr`` -- which will *never* go away -- and is just as stable, since it doesn't constitute a rewrite. +To keep repetition low and this document at a reasonable size, the ``attr`` namespace is `documented on a separate page <api-attr>`, though. Core ---- -.. note:: - - Please note that the ``attrs`` namespace has been added in version 21.3.0. - Most of the objects are simply re-imported from ``attr``. - Therefore if a class, method, or function claims that it has been added in an older version, it is only available in the ``attr`` namespace. - .. autodata:: attrs.NOTHING + :no-value: .. autofunction:: attrs.define -.. function:: attrs.mutable(same_as_define) +.. function:: mutable(same_as_define) - Alias for `attrs.define`. + Same as `attrs.define`. .. versionadded:: 20.1.0 -.. function:: attrs.frozen(same_as_define) +.. function:: frozen(same_as_define) Behaves the same as `attrs.define` but sets *frozen=True* and *on_setattr=None*. .. versionadded:: 20.1.0 -.. autofunction:: attrs.field - -.. function:: define - - Old import path for `attrs.define`. - -.. function:: mutable - - Old import path for `attrs.mutable`. - -.. function:: frozen - - Old import path for `attrs.frozen`. +.. autofunction:: field -.. function:: field - - Old import path for `attrs.field`. - -.. autoclass:: attrs.Attribute +.. autoclass:: Attribute :members: evolve For example: .. doctest:: - >>> import attr - >>> @attr.s - ... class C(object): - ... x = attr.ib() - >>> attr.fields(C).x - Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) + >>> import attrs + >>> from attrs import define, field + + >>> @define + ... class C: + ... x = field() + >>> attrs.fields(C).x + Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x') -.. autofunction:: attrs.make_class +.. autofunction:: make_class This is handy if you want to programmatically create classes. @@ -85,25 +64,27 @@ Core .. doctest:: - >>> C1 = attr.make_class("C1", ["x", "y"]) + >>> C1 = attrs.make_class("C1", ["x", "y"]) >>> C1(1, 2) C1(x=1, y=2) - >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42), - ... "y": attr.ib(default=attr.Factory(list))}) + >>> C2 = attrs.make_class("C2", { + ... "x": field(default=42), + ... "y": field(factory=list) + ... }) >>> C2() C2(x=42, y=[]) -.. autoclass:: attrs.Factory +.. autoclass:: Factory For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(default=attr.Factory(list)) - ... y = attr.ib(default=attr.Factory( + >>> @define + ... class C: + ... x = field(default=attrs.Factory(list)) + ... y = field(default=attrs.Factory( ... lambda self: set(self.x), ... takes_self=True) ... ) @@ -113,86 +94,11 @@ Core C(x=[1, 2, 3], y={1, 2, 3}) -Classic -~~~~~~~ - -.. data:: attr.NOTHING - - Same as `attrs.NOTHING`. - -.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True) - - .. note:: - - ``attrs`` also comes with a serious business alias ``attr.attrs``. - - For example: - - .. doctest:: - - >>> import attr - >>> @attr.s - ... class C(object): - ... _private = attr.ib() - >>> C(private=42) - C(_private=42) - >>> class D(object): - ... def __init__(self, x): - ... self.x = x - >>> D(1) - <D object at ...> - >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) - >>> D(1) - D(x=1) - >>> @attr.s(auto_exc=True) - ... class Error(Exception): - ... x = attr.ib() - ... y = attr.ib(default=42, init=False) - >>> Error("foo") - Error(x='foo', y=42) - >>> raise Error("foo") - Traceback (most recent call last): - ... - Error: ('foo', 42) - >>> raise ValueError("foo", 42) # for comparison - Traceback (most recent call last): - ... - ValueError: ('foo', 42) - - -.. autofunction:: attr.ib - - .. note:: - - ``attrs`` also comes with a serious business alias ``attr.attrib``. - - The object returned by `attr.ib` also allows for setting the default and the validator using decorators: - - .. doctest:: - - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - ... @x.validator - ... def _any_name_except_a_name_of_an_attribute(self, attribute, value): - ... if value < 0: - ... raise ValueError("x must be positive") - ... @y.default - ... def _any_name_except_a_name_of_an_attribute(self): - ... return self.x + 1 - >>> C(1) - C(x=1, y=2) - >>> C(-1) - Traceback (most recent call last): - ... - ValueError: x must be positive - - - Exceptions ---------- +.. module:: attrs.exceptions + All exceptions are available from both ``attr.exceptions`` and ``attrs.exceptions`` and are the same thing. That means that it doesn't matter from from which namespace they've been raised and/or caught: @@ -205,15 +111,15 @@ That means that it doesn't matter from from which namespace they've been raised ... print("this works!") this works! -.. autoexception:: attrs.exceptions.PythonTooOldError -.. autoexception:: attrs.exceptions.FrozenError -.. autoexception:: attrs.exceptions.FrozenInstanceError -.. autoexception:: attrs.exceptions.FrozenAttributeError -.. autoexception:: attrs.exceptions.AttrsAttributeNotFoundError -.. autoexception:: attrs.exceptions.NotAnAttrsClassError -.. autoexception:: attrs.exceptions.DefaultAlreadySetError -.. autoexception:: attrs.exceptions.UnannotatedAttributeError -.. autoexception:: attrs.exceptions.NotCallableError +.. autoexception:: PythonTooOldError +.. autoexception:: FrozenError +.. autoexception:: FrozenInstanceError +.. autoexception:: FrozenAttributeError +.. autoexception:: AttrsAttributeNotFoundError +.. autoexception:: NotAnAttrsClassError +.. autoexception:: DefaultAlreadySetError +.. autoexception:: NotCallableError +.. autoexception:: UnannotatedAttributeError For example:: @@ -228,12 +134,11 @@ That means that it doesn't matter from from which namespace they've been raised Helpers ------- -``attrs`` comes with a bunch of helper methods that make working with it easier: +*attrs* comes with a bunch of helper methods that make working with it easier: -.. autofunction:: attrs.cmp_using -.. function:: attr.cmp_using +.. currentmodule:: attrs - Same as `attrs.cmp_using`. +.. autofunction:: attrs.cmp_using .. autofunction:: attrs.fields @@ -241,21 +146,17 @@ Helpers .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() + >>> @define + ... class C: + ... x = field() + ... y = field() >>> attrs.fields(C) - (Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)) + (Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')) >>> attrs.fields(C)[1] - Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) + Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y') >>> attrs.fields(C).y is attrs.fields(C)[1] True -.. function:: attr.fields - - Same as `attrs.fields`. - .. autofunction:: attrs.fields_dict For example: @@ -263,20 +164,16 @@ Helpers .. doctest:: >>> @attr.s - ... class C(object): + ... class C: ... x = attr.ib() ... y = attr.ib() >>> attrs.fields_dict(C) - {'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)} + {'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='x'), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y')} >>> attr.fields_dict(C)['y'] - Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) + Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='y') >>> attrs.fields_dict(C)['y'] is attrs.fields(C).y True -.. function:: attr.fields_dict - - Same as `attrs.fields_dict`. - .. autofunction:: attrs.has For example: @@ -284,17 +181,13 @@ Helpers .. doctest:: >>> @attr.s - ... class C(object): + ... class C: ... pass >>> attr.has(C) True >>> attr.has(object) False -.. function:: attr.has - - Same as `attrs.has`. - .. autofunction:: attrs.resolve_types For example: @@ -302,12 +195,12 @@ Helpers .. doctest:: >>> import typing - >>> @attrs.define + >>> @define ... class A: ... a: typing.List['A'] ... b: 'B' ... - >>> @attrs.define + >>> @define ... class B: ... a: A ... @@ -322,68 +215,55 @@ Helpers >>> attrs.fields(A).b.type <class 'B'> -.. function:: attr.resolve_types - - Same as `attrs.resolve_types`. - .. autofunction:: attrs.asdict For example: .. doctest:: - >>> @attrs.define + >>> @define ... class C: ... x: int ... y: int >>> attrs.asdict(C(1, C(2, 3))) {'x': 1, 'y': {'x': 2, 'y': 3}} -.. autofunction:: attr.asdict - .. autofunction:: attrs.astuple For example: .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attr.field() - ... y = attr.field() + ... x = field() + ... y = field() >>> attrs.astuple(C(1,2)) (1, 2) -.. autofunction:: attr.astuple - - -``attrs`` includes some handy helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`: - -.. autofunction:: attrs.filters.include - -.. autofunction:: attrs.filters.exclude +.. module:: attrs.filters -.. function:: attr.filters.include +*attrs* includes helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`: - Same as `attrs.filters.include`. +.. autofunction:: include -.. function:: attr.filters.exclude - - Same as `attrs.filters.exclude`. +.. autofunction:: exclude See :func:`attrs.asdict` for examples. -All objects from ``attrs.filters`` are also available from ``attr.filters``. +All objects from ``attrs.filters`` are also available from ``attr.filters`` (it's the same module in a different namespace). ---- +.. currentmodule:: attrs + .. autofunction:: attrs.evolve For example: .. doctest:: - >>> @attrs.define + >>> @define ... class C: ... x: int ... y: int @@ -403,19 +283,15 @@ All objects from ``attrs.filters`` are also available from ``attr.filters``. * attributes with ``init=False`` can't be set with ``evolve``. * the usual ``__init__`` validators will validate the new values. -.. function:: attr.evolve - - Same as `attrs.evolve`. - .. autofunction:: attrs.validate For example: .. doctest:: - >>> @attrs.define(on_setattr=attrs.setters.NO_OP) + >>> @define(on_setattr=attrs.setters.NO_OP) ... class C: - ... x = attrs.field(validator=attrs.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) >>> i = C(1) >>> i.x = "1" >>> attrs.validate(i) @@ -423,26 +299,16 @@ All objects from ``attrs.filters`` are also available from ``attr.filters``. ... TypeError: ("'x' must be <class 'int'> (got '1' that is a <class 'str'>).", ...) -.. function:: attr.validate - Same as `attrs.validate`. - - -Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact: - -.. autofunction:: set_run_validators - -.. autofunction:: get_run_validators - - -.. _api_validators: +.. _api-validators: Validators ---------- -``attrs`` comes with some common validators in the ``attrs.validators`` module. -All objects from ``attrs.converters`` are also available from ``attr.converters``. +.. module:: attrs.validators +*attrs* comes with some common validators in the ``attrs.validators`` module. +All objects from ``attrs.validators`` are also available from ``attr.validators`` (it's the same module in a different namespace). .. autofunction:: attrs.validators.lt @@ -450,9 +316,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.lt(42)) + ... x = field(validator=attrs.validators.lt(42)) >>> C(41) C(x=41) >>> C(42) @@ -466,9 +332,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define - ... class C(object): - ... x = attrs.field(validator=attr.validators.le(42)) + >>> @define + ... class C: + ... x = field(validator=attrs.validators.le(42)) >>> C(42) C(x=42) >>> C(43) @@ -482,7 +348,7 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: ... x = attrs.field(validator=attrs.validators.ge(42)) >>> C(42) @@ -498,9 +364,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attr.field(validator=attrs.validators.gt(42)) + ... x = field(validator=attrs.validators.gt(42)) >>> C(43) C(x=43) >>> C(42) @@ -514,9 +380,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.max_len(4)) + ... x = field(validator=attrs.validators.max_len(4)) >>> C("spam") C(x='spam') >>> C("bacon") @@ -524,15 +390,31 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` ... ValueError: ("Length of 'x' must be <= 4: 5") +.. autofunction:: attrs.validators.min_len + + For example: + + .. doctest:: + + >>> @define + ... class C: + ... x = field(validator=attrs.validators.min_len(1)) + >>> C("bacon") + C(x='bacon') + >>> C("") + Traceback (most recent call last): + ... + ValueError: ("Length of 'x' must be => 1: 0") + .. autofunction:: attrs.validators.instance_of For example: .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -550,24 +432,24 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> import enum - >>> class State(enum.Enum): - ... ON = "on" - ... OFF = "off" - >>> @attrs.define - ... class C: - ... state = attrs.field(validator=attrs.validators.in_(State)) - ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3])) - >>> C(State.ON, 1) - C(state=<State.ON: 'on'>, val=1) - >>> C("on", 1) - Traceback (most recent call last): - ... - ValueError: 'state' must be in <enum 'State'> (got 'on') - >>> C(State.ON, 4) - Traceback (most recent call last): - ... - ValueError: 'val' must be in [1, 2, 3] (got 4) + >>> import enum + >>> class State(enum.Enum): + ... ON = "on" + ... OFF = "off" + >>> @define + ... class C: + ... state = field(validator=attrs.validators.in_(State)) + ... val = field(validator=attrs.validators.in_([1, 2, 3])) + >>> C(State.ON, 1) + C(state=<State.ON: 'on'>, val=1) + >>> C("On", 1) + Traceback (most recent call last): + ... + ValueError: 'state' must be in <enum 'State'> (got 'On'), Attribute(name='state', default=NOTHING, validator=<in_ validator with options <enum 'State'>>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), <enum 'State'>, 'on') + >>> C(State.ON, 4) + Traceback (most recent call last): + ... + ValueError: 'val' must be in [1, 2, 3] (got 4), Attribute(name='val', default=NOTHING, validator=<in_ validator with options [1, 2, 3]>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), [1, 2, 3], 4) .. autofunction:: attrs.validators.provides @@ -577,8 +459,34 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` Thus the following two statements are equivalent:: - x = attrs.field(validator=attrs.validators.and_(v1, v2, v3)) - x = attrs.field(validator=[v1, v2, v3]) + x = field(validator=attrs.validators.and_(v1, v2, v3)) + x = field(validator=[v1, v2, v3]) + +.. autofunction:: attrs.validators.not_ + + For example: + + .. doctest:: + + >>> reserved_names = {"id", "time", "source"} + >>> @define + ... class Measurement: + ... tags = field( + ... validator=attrs.validators.deep_mapping( + ... key_validator=attrs.validators.not_( + ... attrs.validators.in_(reserved_names), + ... msg="reserved tag key", + ... ), + ... value_validator=attrs.validators.instance_of((str, int)), + ... ) + ... ) + >>> Measurement(tags={"source": "universe"}) + Traceback (most recent call last): + ... + ValueError: ("reserved tag key", Attribute(name='tags', default=NOTHING, validator=<not_ validator wrapping <in_ validator with options {'id', 'time', 'source'}>, capturing (<class 'ValueError'>, <class 'TypeError'>)>, type=None, kw_only=False), <in_ validator with options {'id', 'time', 'source'}>, {'source_': 'universe'}, (<class 'ValueError'>, <class 'TypeError'>)) + >>> Measurement(tags={"source_": "universe"}) + Measurement(tags={'source_': 'universe'}) + .. autofunction:: attrs.validators.optional @@ -586,9 +494,12 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.optional(attr.validators.instance_of(int))) + ... x = field( + ... validator=attrs.validators.optional( + ... attrs.validators.instance_of(int) + ... )) >>> C(42) C(x=42) >>> C("42") @@ -605,9 +516,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.is_callable()) + ... x = field(validator=attrs.validators.is_callable()) >>> C(isinstance) C(x=<built-in function isinstance>) >>> C("not a callable") @@ -622,10 +533,10 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class User: - ... email = attrs.field(validator=attrs.validators.matches_re( - ... "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")) + ... email = field(validator=attrs.validators.matches_re( + ... r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")) >>> User(email="user@example.com") User(email='user@example.com') >>> User(email="user@example.com@test.com") @@ -640,11 +551,11 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.deep_iterable( - ... member_validator=attrs.validators.instance_of(int), - ... iterable_validator=attrs.validators.instance_of(list) + ... x = field(validator=attrs.validators.deep_iterable( + ... member_validator=attrs.validators.instance_of(int), + ... iterable_validator=attrs.validators.instance_of(list) ... )) >>> C(x=[1, 2, 3]) C(x=[1, 2, 3]) @@ -664,12 +575,12 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attrs.define + >>> @define ... class C: - ... x = attrs.field(validator=attrs.validators.deep_mapping( - ... key_validator=attrs.validators.instance_of(str), - ... value_validator=attrs.validators.instance_of(int), - ... mapping_validator=attrs.validators.instance_of(dict) + ... x = field(validator=attrs.validators.deep_mapping( + ... key_validator=attrs.validators.instance_of(str), + ... value_validator=attrs.validators.instance_of(int), + ... mapping_validator=attrs.validators.instance_of(dict) ... )) >>> C(x={"a": 1, "b": 2}) C(x={'a': 1, 'b': 2}) @@ -698,16 +609,18 @@ Validators can be both globally and locally disabled: Converters ---------- -All objects from ``attrs.converters`` are also available from ``attr.converters``. +.. module:: attrs.converters + +All objects from ``attrs.converters`` are also available from ``attr.converters`` (it's the same module in a different namespace). .. autofunction:: attrs.converters.pipe - For convenience, it's also possible to pass a list to `attr.ib`'s converter argument. + For convenience, it's also possible to pass a list to `attrs.field` / `attr.ib`'s converter arguments. Thus the following two statements are equivalent:: - x = attr.ib(converter=attr.converter.pipe(c1, c2, c3)) - x = attr.ib(converter=[c1, c2, c3]) + x = attrs.field(converter=attrs.converter.pipe(c1, c2, c3)) + x = attrs.field(converter=[c1, c2, c3]) .. autofunction:: attrs.converters.optional @@ -715,9 +628,9 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(converter=attr.converters.optional(int)) + >>> @define + ... class C: + ... x = field(converter=attrs.converters.optional(int)) >>> C(None) C(x=None) >>> C(42) @@ -730,10 +643,10 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib( - ... converter=attr.converters.default_if_none("") + >>> @define + ... class C: + ... x = field( + ... converter=attrs.converters.default_if_none("") ... ) >>> C(None) C(x='') @@ -745,10 +658,10 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib( - ... converter=attr.converters.to_bool + >>> @define + ... class C: + ... x = field( + ... converter=attrs.converters.to_bool ... ) >>> C("yes") C(x=True) @@ -766,23 +679,32 @@ All objects from ``attrs.converters`` are also available from ``attr.converters` Setters ------- +.. module:: attrs.setters + These are helpers that you can use together with `attrs.define`'s and `attrs.fields`'s ``on_setattr`` arguments. -All setters in ``attrs.setters`` are also available from ``attr.setters``. +All setters in ``attrs.setters`` are also available from ``attr.setters`` (it's the same module in a different namespace). + +.. autofunction:: frozen +.. autofunction:: validate +.. autofunction:: convert +.. autofunction:: pipe -.. autofunction:: attrs.setters.frozen -.. autofunction:: attrs.setters.validate -.. autofunction:: attrs.setters.convert -.. autofunction:: attrs.setters.pipe -.. autodata:: attrs.setters.NO_OP +.. data:: NO_OP + + Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. + + Does not work in `attrs.setters.pipe` or within lists. + + .. versionadded:: 20.1.0 For example, only ``x`` is frozen here: .. doctest:: - >>> @attrs.define(on_setattr=attr.setters.frozen) + >>> @define(on_setattr=attr.setters.frozen) ... class C: - ... x = attr.field() - ... y = attr.field(on_setattr=attr.setters.NO_OP) + ... x = field() + ... y = field(on_setattr=attr.setters.NO_OP) >>> c = C(1, 2) >>> c.y = 3 >>> c.y @@ -793,34 +715,3 @@ All setters in ``attrs.setters`` are also available from ``attr.setters``. attrs.exceptions.FrozenAttributeError: () N.B. Please use `attrs.define`'s *frozen* argument (or `attrs.frozen`) to freeze whole classes; it is more efficient. - - -Deprecated APIs ---------------- - -.. _version-info: - -To help you write backward compatible code that doesn't throw warnings on modern releases, the ``attr`` module has an ``__version_info__`` attribute as of version 19.2.0. -It behaves similarly to `sys.version_info` and is an instance of `VersionInfo`: - -.. autoclass:: VersionInfo - - With its help you can write code like this: - - >>> if getattr(attr, "__version_info__", (0,)) >= (19, 2): - ... cmp_off = {"eq": False} - ... else: - ... cmp_off = {"cmp": False} - >>> cmp_off == {"eq": False} - True - >>> @attr.s(**cmp_off) - ... class C(object): - ... pass - - ----- - -The serious business aliases used to be called ``attr.attributes`` and ``attr.attr``. -There are no plans to remove them but they shouldn't be used in new code. - -.. autofunction:: assoc diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/changelog.md b/tests/wpt/tests/tools/third_party/attrs/docs/changelog.md new file mode 100644 index 00000000000..3c8d4d8b356 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/changelog.md @@ -0,0 +1,11 @@ +```{include} ../CHANGELOG.md +:end-before: Changes for the upcoming release can be found +``` + + ```{towncrier-draft-entries} +main + ``` + +```{include} ../CHANGELOG.md +:start-after: towncrier release notes start --> +``` diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/changelog.rst b/tests/wpt/tests/tools/third_party/attrs/docs/changelog.rst deleted file mode 100644 index 565b0521d0c..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CHANGELOG.rst diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/comparison.md b/tests/wpt/tests/tools/third_party/attrs/docs/comparison.md new file mode 100644 index 00000000000..79786e9e19f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/comparison.md @@ -0,0 +1,69 @@ +# Comparison + +By default, two instances of *attrs* classes are equal if they have the same type and all their fields are equal. +For that, *attrs* writes `__eq__` and `__ne__` methods for you. + +Additionally, if you pass `order=True`, *attrs* will also create a complete set of ordering methods: `__le__`, `__lt__`, `__ge__`, and `__gt__`. + +Both for equality and order, *attrs* will: + +- Check if the types of the instances you're comparing are equal, +- if so, create a tuple of all field values for each instance, +- and finally perform the desired comparison operation on those tuples. + +[^default]: That's the default if you use the {func}`attr.s` decorator, but not with {func}`~attrs.define`. + +(custom-comparison)= + +## Customization + +As with other features, you can exclude fields from being involved in comparison operations: + +```{doctest} +>>> from attrs import define, field +>>> @define +... class C: +... x: int +... y: int = field(eq=False) + +>>> C(1, 2) == C(1, 3) +True +``` + +Additionally you can also pass a *callable* instead of a bool to both *eq* and *order*. +It is then used as a key function like you may know from {func}`sorted`: + +```{doctest} +>>> @define +... class S: +... x: str = field(eq=str.lower) + +>>> S("foo") == S("FOO") +True + +>>> @define(order=True) +... class C: +... x: str = field(order=int) + +>>> C("10") > C("2") +True +``` + +This is especially useful when you have fields with objects that have atypical comparison properties. +Common examples of such objects are [NumPy arrays](https://github.com/python-attrs/attrs/issues/435). + +To save you unnecessary boilerplate, *attrs* comes with the {func}`attrs.cmp_using` helper to create such functions. +For NumPy arrays it would look like this: + +```python +import numpy + +@define +class C: + an_array = field(eq=attrs.cmp_using(eq=numpy.array_equal)) +``` + +:::{warning} +Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in {func}`~attrs.define` (but not in {func}`attr.s`). +You can set both at once by using the *cmp* argument that we've undeprecated just for this use-case. +::: diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/comparison.rst b/tests/wpt/tests/tools/third_party/attrs/docs/comparison.rst deleted file mode 100644 index 760124ca3bc..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/comparison.rst +++ /dev/null @@ -1,66 +0,0 @@ -Comparison -========== - -By default, two instances of ``attrs`` classes are equal if all their fields are equal. -For that, ``attrs`` writes ``__eq__`` and ``__ne__`` methods for you. - -Additionally, if you pass ``order=True`` (which is the default if you use the `attr.s` decorator), ``attrs`` will also create a full set of ordering methods that are based on the defined fields: ``__le__``, ``__lt__``, ``__ge__``, and ``__gt__``. - - -.. _custom-comparison: - -Customization -------------- - -As with other features, you can exclude fields from being involved in comparison operations: - -.. doctest:: - - >>> from attr import define, field - - >>> @define - ... class C: - ... x: int - ... y: int = field(eq=False) - - >>> C(1, 2) == C(1, 3) - True - -Additionally you can also pass a *callable* instead of a bool to both *eq* and *order*. -It is then used as a key function like you may know from `sorted`: - -.. doctest:: - - >>> from attr import define, field - - >>> @define - ... class S: - ... x: str = field(eq=str.lower) - - >>> S("foo") == S("FOO") - True - - >>> @define(order=True) - ... class C: - ... x: str = field(order=int) - - >>> C("10") > C("2") - True - -This is especially useful when you have fields with objects that have atypical comparison properties. -Common examples of such objects are `NumPy arrays <https://github.com/python-attrs/attrs/issues/435>`_. - -To save you unnecessary boilerplate, ``attrs`` comes with the `attr.cmp_using` helper to create such functions. -For NumPy arrays it would look like this:: - - import numpy - - @define(order=False) - class C: - an_array = field(eq=attr.cmp_using(eq=numpy.array_equal)) - - -.. warning:: - - Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in `attrs.define` (but not in `attr.s`). - You can set both at once by using the *cmp* argument that we've undeprecated just for this use-case. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/conf.py b/tests/wpt/tests/tools/third_party/attrs/docs/conf.py index 0cc80be6a68..b92354a6fd4 100644 --- a/tests/wpt/tests/tools/third_party/attrs/docs/conf.py +++ b/tests/wpt/tests/tools/third_party/attrs/docs/conf.py @@ -1,6 +1,12 @@ # SPDX-License-Identifier: MIT from importlib import metadata +from pathlib import Path + + +# -- Path setup ----------------------------------------------------------- + +PROJECT_ROOT_DIR = Path(__file__).parents[1].resolve() # -- General configuration ------------------------------------------------ @@ -12,9 +18,8 @@ from attr import define, frozen, field, validators, Factory linkcheck_ignore = [ # We run into GitHub's rate limits. r"https://github.com/.*/(issues|pull)/\d+", - # It never finds the anchor even though it's there. - "https://github.com/microsoft/pyright/blob/main/specs/" - "dataclass_transforms.md#attrs", + # Rate limits and the latest tag is missing anyways on release. + "https://github.com/python-attrs/attrs/tree/.*", ] # In nitpick mode (-n), still ignore any of the following "broken" references @@ -30,13 +35,20 @@ nitpick_ignore = [ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "notfound.extension", + "sphinxcontrib.towncrier", ] +myst_enable_extensions = [ + "colon_fence", + "smartquotes", + "deflist", +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -58,8 +70,11 @@ copyright = f"2015, {author}" # The full version, including alpha/beta/rc tags. release = metadata.version("attrs") -# The short X.Y version. -version = release.rsplit(".", 1)[0] +if "dev" in release: + release = version = "UNRELEASED" +else: + # The short X.Y version. + version = release.rsplit(".", 1)[0] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -82,7 +97,14 @@ html_theme_options = { "sidebar_hide_name": True, "light_logo": "attrs_logo.svg", "dark_logo": "attrs_logo_white.svg", + "top_of_page_button": None, + "light_css_variables": { + "font-stack": "Inter,sans-serif", + "font-stack--monospace": "BerkeleyMono, MonoLisa, ui-monospace, " + "SFMono-Regular, Menlo, Consolas, Liberation Mono, monospace", + }, } +html_css_files = ["custom.css"] # The name of an image file (within the static path) to use as favicon of the @@ -147,9 +169,15 @@ texinfo_documents = [ epub_description = "Python Clases Without Boilerplate" -intersphinx_mapping = { - "https://docs.python.org/3": None, -} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} # Allow non-local URIs so we can have images in CHANGELOG etc. suppress_warnings = ["image.nonlocal_uri"] + + +# -- Options for sphinxcontrib.towncrier extension ------------------------ + +towncrier_draft_autoversion_mode = "draft" +towncrier_draft_include_empty = True +towncrier_draft_working_directory = PROJECT_ROOT_DIR +towncrier_draft_config_path = "pyproject.toml" diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/docutils.conf b/tests/wpt/tests/tools/third_party/attrs/docs/docutils.conf deleted file mode 100644 index db8ca82c747..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/docutils.conf +++ /dev/null @@ -1,3 +0,0 @@ -[parsers] -[restructuredtext parser] -smart_quotes=yes diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/examples.md b/tests/wpt/tests/tools/third_party/attrs/docs/examples.md new file mode 100644 index 00000000000..0f8301aa597 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/examples.md @@ -0,0 +1,762 @@ +# *attrs* by Example + +## Basics + +The simplest possible usage is: + +```{doctest} +>>> from attrs import define, field +>>> @define +... class Empty: +... pass +>>> Empty() +Empty() +>>> Empty() == Empty() +True +>>> Empty() is Empty() +False +``` + +So in other words: *attrs* is useful even without actual attributes! + +But you'll usually want some data on your classes, so let's add some: + +```{doctest} +>>> @define +... class Coordinates: +... x: int +... y: int +``` + +By default, all features are added, so you immediately have a fully functional data class with a nice `repr` string and comparison methods. + +```{doctest} +>>> c1 = Coordinates(1, 2) +>>> c1 +Coordinates(x=1, y=2) +>>> c2 = Coordinates(x=2, y=1) +>>> c2 +Coordinates(x=2, y=1) +>>> c1 == c2 +False +``` + +As shown, the generated `__init__` method allows for both positional and keyword arguments. + +--- + +Unlike Data Classes, *attrs* doesn't force you to use type annotations. +So, the previous example could also have been written as: + +```{doctest} +>>> @define +... class Coordinates: +... x = field() +... y = field() +>>> Coordinates(1, 2) +Coordinates(x=1, y=2) +``` + +:::{caution} +If a class body contains a field that is defined using {func}`attrs.field` (or {func}`attr.ib`), but **lacks a type annotation**, *attrs* switches to a no-typing mode and ignores fields that have type annotations but are not defined using {func}`attrs.field` (or {func}`attr.ib`). +::: + +--- + +For private attributes, *attrs* will strip the leading underscores for keyword arguments: + +```{doctest} +>>> @define +... class C: +... _x: int +>>> C(x=1) +C(_x=1) +``` + +If you want to initialize your private attributes yourself, you can do that too: + +```{doctest} +>>> @define +... class C: +... _x: int = field(init=False, default=42) +>>> C() +C(_x=42) +>>> C(23) +Traceback (most recent call last): + ... +TypeError: __init__() takes exactly 1 argument (2 given) +``` + +If you prefer to expose your privates, you can use keyword argument aliases: + +```{doctest} +>>> @define +... class C: +... _x: int = field(alias="_x") +>>> C(_x=1) +C(_x=1) +``` + +An additional way of defining attributes is supported too. +This is useful in times when you want to enhance classes that are not yours (nice `__repr__` for Django models anyone?): + +```{doctest} +>>> class SomethingFromSomeoneElse: +... def __init__(self, x): +... self.x = x +>>> SomethingFromSomeoneElse = define( +... these={ +... "x": field() +... }, init=False)(SomethingFromSomeoneElse) +>>> SomethingFromSomeoneElse(1) +SomethingFromSomeoneElse(x=1) +``` + +[Subclassing is bad for you](https://www.youtube.com/watch?v=3MNVP9-hglc), but *attrs* will still do what you'd hope for: + +```{doctest} +>>> @define(slots=False) +... class A: +... a: int +... def get_a(self): +... return self.a +>>> @define(slots=False) +... class B: +... b: int +>>> @define(slots=False) +... class C(B, A): +... c: int +>>> i = C(1, 2, 3) +>>> i +C(a=1, b=2, c=3) +>>> i == C(1, 2, 3) +True +>>> i.get_a() +1 +``` + +{term}`Slotted classes <slotted classes>`, which are the default for the new APIs, don't play well with multiple inheritance so we don't use them in the example. + +The order of the attributes is defined by the [MRO](https://www.python.org/download/releases/2.3/mro/). + + +### Keyword-only Attributes + +You can also add [keyword-only](https://docs.python.org/3/glossary.html#keyword-only-parameter) attributes: + +```{doctest} +>>> @define +... class A: +... a: int = field(kw_only=True) +>>> A() +Traceback (most recent call last): +... +TypeError: A() missing 1 required keyword-only argument: 'a' +>>> A(a=1) +A(a=1) +``` + +`kw_only` may also be specified at decorator level, and will apply to all attributes: + +```{doctest} +>>> @define(kw_only=True) +... class A: +... a: int +... b: int +>>> A(1, 2) +Traceback (most recent call last): +... +TypeError: __init__() takes 1 positional argument but 3 were given +>>> A(a=1, b=2) +A(a=1, b=2) +``` + +If you create an attribute with `init=False`, the `kw_only` argument is ignored. + +Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values: + +```{doctest} +>>> @define +... class A: +... a: int = 0 +>>> @define +... class B(A): +... b: int = field(kw_only=True) +>>> B(b=1) +B(a=0, b=1) +>>> B() +Traceback (most recent call last): +... +TypeError: B() missing 1 required keyword-only argument: 'b' +``` + +If you don't set `kw_only=True`, then there is no valid attribute ordering, and you'll get an error: + +```{doctest} +>>> @define +... class A: +... a: int = 0 +>>> @define +... class B(A): +... b: int +Traceback (most recent call last): +... +ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=int, kw_only=False) +``` + +(asdict)= + +## Converting to Collections Types + +When you have a class with data, it often is very convenient to transform that class into a {class}`dict` (for example if you want to serialize it to JSON): + +```{doctest} +>>> from attrs import asdict +>>> asdict(Coordinates(x=1, y=2)) +{'x': 1, 'y': 2} +``` + +Some fields cannot or should not be transformed. +For that, {func}`attrs.asdict` offers a callback that decides whether an attribute should be included: + +```{doctest} +>>> @define +... class User: +... email: str +... password: str + +>>> @define +... class UserList: +... users: list[User] + +>>> asdict(UserList([User("jane@doe.invalid", "s33kred"), +... User("joe@doe.invalid", "p4ssw0rd")]), +... filter=lambda attr, value: attr.name != "password") +{'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]} +``` + +For the common case where you want to [`include`](attrs.filters.include) or [`exclude`](attrs.filters.exclude) certain types, string name or attributes, *attrs* ships with a few helpers: + +```{doctest} +>>> from attrs import asdict, filters, fields + +>>> @define +... class User: +... login: str +... password: str +... email: str +... id: int + +>>> asdict( +... User("jane", "s33kred", "jane@example.com", 42), +... filter=filters.exclude(fields(User).password, "email", int)) +{'login': 'jane'} + +>>> @define +... class C: +... x: str +... y: str +... z: int + +>>> asdict(C("foo", "2", 3), +... filter=filters.include(int, fields(C).x)) +{'x': 'foo', 'z': 3} + +>>> asdict(C("foo", "2", 3), +... filter=filters.include(fields(C).x, "z")) +{'x': 'foo', 'z': 3} +``` + +:::{note} +Though using string names directly is convenient, mistyping attribute names will silently do the wrong thing and neither Python nor your type checker can help you. +{func}`attrs.fields()` will raise an `AttributeError` when the field doesn't exist while literal string names won't. +Using {func}`attrs.fields()` to get attributes is worth being recommended in most cases. + +```{doctest} +>>> asdict( +... User("jane", "s33kred", "jane@example.com", 42), +... filter=filters.exclude("passwd") +... ) +{'login': 'jane', 'password': 's33kred', 'email': 'jane@example.com', 'id': 42} + +>>> asdict( +... User("jane", "s33kred", "jane@example.com", 42), +... filter=fields(User).passwd +... ) +Traceback (most recent call last): +... +AttributeError: 'UserAttributes' object has no attribute 'passwd'. Did you mean: 'password'? +``` +::: + +Other times, all you want is a tuple and *attrs* won't let you down: + +```{doctest} +>>> import sqlite3 +>>> from attrs import astuple + +>>> @define +... class Foo: +... a: int +... b: int + +>>> foo = Foo(2, 3) +>>> with sqlite3.connect(":memory:") as conn: +... c = conn.cursor() +... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS +... c.execute("INSERT INTO foo VALUES (?, ?)", astuple(foo)) #doctest: +ELLIPSIS +... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone()) +<sqlite3.Cursor object at ...> +<sqlite3.Cursor object at ...> +>>> foo == foo2 +True +``` + +For more advanced transformations and conversions, we recommend you look at a companion library (such as [*cattrs*](https://catt.rs/)). + + +## Defaults + +Sometimes you want to have default values for your initializer. +And sometimes you even want mutable objects as default values (ever accidentally used `def f(arg=[])`?). +*attrs* has you covered in both cases: + +```{doctest} +>>> import collections + +>>> @define +... class Connection: +... socket: int +... @classmethod +... def connect(cls, db_string): +... # ... connect somehow to db_string ... +... return cls(socket=42) + +>>> @define +... class ConnectionPool: +... db_string: str +... pool: collections.deque = Factory(collections.deque) +... debug: bool = False +... def get_connection(self): +... try: +... return self.pool.pop() +... except IndexError: +... if self.debug: +... print("New connection!") +... return Connection.connect(self.db_string) +... def free_connection(self, conn): +... if self.debug: +... print("Connection returned!") +... self.pool.appendleft(conn) +... +>>> cp = ConnectionPool("postgres://localhost") +>>> cp +ConnectionPool(db_string='postgres://localhost', pool=deque([]), debug=False) +>>> conn = cp.get_connection() +>>> conn +Connection(socket=42) +>>> cp.free_connection(conn) +>>> cp +ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False) +``` + +More information on why class methods for constructing objects are awesome can be found in this insightful [blog post](https://web.archive.org/web/20210130220433/http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html). + +Default factories can also be set using the `factory` argument to {func}`~attrs.field`, and using a decorator. +The method receives the partially initialized instance which enables you to base a default value on other attributes: + +```{doctest} +>>> @define +... class C: +... x: int = 1 +... y: int = field() +... @y.default +... def _any_name_except_a_name_of_an_attribute(self): +... return self.x + 1 +... z: list = field(factory=list) +>>> C() +C(x=1, y=2, z=[]) +``` + +Please keep in mind that the decorator approach *only* works if the attribute in question has a {func}`~attrs.field` assigned to it. +As a result, annotating an attribute with a type is *not* enough if you use `@default`. + +(examples-validators)= + +## Validators + +Although your initializers should do as little as possible (ideally: just initialize your instance according to the arguments!), it can come in handy to do some kind of validation on the arguments. + +*attrs* offers two ways to define validators for each attribute and it's up to you to choose which one suits your style and project better. + +You can use a decorator: + +```{doctest} +>>> @define +... class C: +... x: int = field() +... @x.validator +... def check(self, attribute, value): +... if value > 42: +... raise ValueError("x must be smaller or equal to 42") +>>> C(42) +C(x=42) +>>> C(43) +Traceback (most recent call last): + ... +ValueError: x must be smaller or equal to 42 +``` + +...or a callable... + +```{doctest} +>>> from attrs import validators + +>>> def x_smaller_than_y(instance, attribute, value): +... if value >= instance.y: +... raise ValueError("'x' has to be smaller than 'y'!") +>>> @define +... class C: +... x: int = field(validator=[validators.instance_of(int), +... x_smaller_than_y]) +... y: int +>>> C(x=3, y=4) +C(x=3, y=4) +>>> C(x=4, y=3) +Traceback (most recent call last): + ... +ValueError: 'x' has to be smaller than 'y'! +``` + +...or both at once: + +```{doctest} +>>> @define +... class C: +... x: int = field(validator=validators.instance_of(int)) +... @x.validator +... def fits_byte(self, attribute, value): +... if not 0 <= value < 256: +... raise ValueError("value out of bounds") +>>> C(128) +C(x=128) +>>> C("128") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=int, converter=None, kw_only=False), <class 'int'>, '128') +>>> C(256) +Traceback (most recent call last): + ... +ValueError: value out of bounds +``` + +Please note that the decorator approach only works if -- and only if! -- the attribute in question has a {func}`~attrs.field` assigned. +Therefore if you use `@validator`, it is *not* enough to annotate said attribute with a type. + +*attrs* ships with a bunch of validators, make sure to [check them out](api-validators) before writing your own: + +```{doctest} +>>> @define +... class C: +... x: int = field(validator=validators.instance_of(int)) +>>> C(42) +C(x=42) +>>> C("42") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42') +``` + +If using the old-school {func}`attr.s` decorator, validators only run on initialization by default. +If using the newer {func}`attrs.define` and friends, validators run on initialization *and* on attribute setting. +This behavior can be changed using the *on_setattr* argument. + +Check out {ref}`validators` for more details. + + +## Conversion + +Attributes can have a `converter` function specified, which will be called with the attribute's passed-in value to get a new value to use. +This can be useful for doing type-conversions on values that you don't want to force your callers to do. + +```{doctest} +>>> @define +... class C: +... x: int = field(converter=int) +>>> o = C("1") +>>> o.x +1 +>>> o.x = "2" +>>> o.x +2 +``` + +If using the old-school {func}`attr.s` decorator, converters only run on initialization by default. +If using the newer {func}`attrs.define` and friends, converters run on initialization *and* on attribute setting. +This behavior can be changed using the *on_setattr* argument. + +Check out {ref}`converters` for more details. + +(metadata)= + +## Metadata + +All *attrs* attributes may include arbitrary metadata in the form of a read-only dictionary. + +```{doctest} +>>> from attrs import fields + +>>> @define +... class C: +... x = field(metadata={'my_metadata': 1}) +>>> fields(C).x.metadata +mappingproxy({'my_metadata': 1}) +>>> fields(C).x.metadata['my_metadata'] +1 +``` + +Metadata is not used by *attrs*, and is meant to enable rich functionality in third-party libraries. +The metadata dictionary follows the normal dictionary rules: +Keys need to be hashable, and both keys and values are recommended to be immutable. + +If you're the author of a third-party library with *attrs* integration, please see [*Extending Metadata*](extending-metadata). + + +## Types + +*attrs* also allows you to associate a type with an attribute using either the *type* argument to using {pep}`526`-annotations or {func}`attrs.field`/{func}`attr.ib`: + +```{doctest} +>>> @define +... class C: +... x: int +>>> fields(C).x.type +<class 'int'> + +>>> import attr +>>> @attr.s +... class C: +... x = attr.ib(type=int) +>>> fields(C).x.type +<class 'int'> +``` + +If you don't mind annotating *all* attributes, you can even drop the `attrs.field` and assign default values instead: + +```{doctest} +>>> import typing + +>>> @define +... class AutoC: +... cls_var: typing.ClassVar[int] = 5 # this one is ignored +... l: list[int] = Factory(list) +... x: int = 1 +... foo: str = "every attrib needs a type if auto_attribs=True" +... bar: typing.Any = None +>>> fields(AutoC).l.type +list[int] +>>> fields(AutoC).x.type +<class 'int'> +>>> fields(AutoC).foo.type +<class 'str'> +>>> fields(AutoC).bar.type +typing.Any +>>> AutoC() +AutoC(l=[], x=1, foo='every attrib needs a type if auto_attribs=True', bar=None) +>>> AutoC.cls_var +5 +``` + +The generated `__init__` method will have an attribute called `__annotations__` that contains this type information. + +If your annotations contain strings (e.g. forward references), +you can resolve these after all references have been defined by using {func}`attrs.resolve_types`. +This will replace the *type* attribute in the respective fields. + +```{doctest} +>>> from attrs import resolve_types + +>>> @define +... class A: +... a: 'list[A]' +... b: 'B' +... +>>> @define +... class B: +... a: A +... +>>> fields(A).a.type +'list[A]' +>>> fields(A).b.type +'B' +>>> resolve_types(A, globals(), locals()) +<class 'A'> +>>> fields(A).a.type +list[A] +>>> fields(A).b.type +<class 'B'> +``` + +:::{note} +If you find yourself using string type annotations to handle forward references, wrap the entire type annotation in quotes instead of only the type you need a forward reference to (so `'list[A]'` instead of `list['A']`). +This is a limitation of the Python typing system. +::: + +:::{warning} +*attrs* itself doesn't have any features that work on top of type metadata. +However it's useful for writing your own validators or serialization frameworks. +::: + + +## Slots + +{term}`Slotted classes <slotted classes>` have several advantages on CPython. +Defining `__slots__` by hand is tedious, in *attrs* it's just a matter of using {func}`attrs.define` or passing `slots=True` to {func}`attr.s`: + +```{doctest} +>>> @define +... class Coordinates: +... x: int +... y: int + +>>> import attr + +>>> @attr.s(slots=True) +... class Coordinates: +... x: int +... y: int +``` + +{func}`~attrs.define` sets `slots=True` by default. + + +## Immutability + +Sometimes you have instances that shouldn't be changed after instantiation. +Immutability is especially popular in functional programming and is generally a very good thing. +If you'd like to enforce it, *attrs* will try to help: + +```{doctest} +>>> from attrs import frozen + +>>> @frozen +... class C: +... x: int +>>> i = C(1) +>>> i.x = 2 +Traceback (most recent call last): + ... +attrs.exceptions.FrozenInstanceError: can't set attribute +>>> i.x +1 +``` + +Please note that true immutability is impossible in Python but it will [get](how-frozen) you 99% there. +By themselves, immutable classes are useful for long-lived objects that should never change; like configurations for example. + +In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes. +In Clojure that function is called [*assoc*](https://clojuredocs.org/clojure.core/assoc) and *attrs* shamelessly imitates it: {func}`attrs.evolve`: + +```{doctest} +>>> from attrs import evolve, frozen + +>>> @frozen +... class C: +... x: int +... y: int +>>> i1 = C(1, 2) +>>> i1 +C(x=1, y=2) +>>> i2 = evolve(i1, y=3) +>>> i2 +C(x=1, y=3) +>>> i1 == i2 +False +``` + + +## Other Goodies + +Sometimes you may want to create a class programmatically. +*attrs* gives you {func}`attrs.make_class` for that: + +```{doctest} +>>> from attrs import make_class +>>> @define +... class C1: +... x = field(type=int) +... y = field() +>>> C2 = make_class("C2", {"x": field(type=int), "y": field()}) +>>> fields(C1) == fields(C2) +True +>>> fields(C1).x.type +<class 'int'> +``` + +You can still have power over the attributes if you pass a dictionary of name: {func}`~attrs.field` mappings and can pass the same arguments as you can to `@attrs.define`: + +```{doctest} +>>> C = make_class("C", {"x": field(default=42), +... "y": field(default=Factory(list))}, +... repr=False) +>>> i = C() +>>> i # no repr added! +<__main__.C object at ...> +>>> i.x +42 +>>> i.y +[] +``` + +If you need to dynamically make a class with {func}`~attrs.make_class` and it needs to be a subclass of something else than {class}`object`, use the `bases` argument: + +```{doctest} +>>> class D: +... def __eq__(self, other): +... return True # arbitrary example +>>> C = make_class("C", {}, bases=(D,), cmp=False) +>>> isinstance(C(), D) +True +``` + +Sometimes, you want to have your class's `__init__` method do more than just +the initialization, validation, etc. that gets done for you automatically when +using `@define`. +To do this, just define a `__attrs_post_init__` method in your class. +It will get called at the end of the generated `__init__` method. + +```{doctest} +>>> @define +... class C: +... x: int +... y: int +... z: int = field(init=False) +... +... def __attrs_post_init__(self): +... self.z = self.x + self.y +>>> obj = C(x=1, y=2) +>>> obj +C(x=1, y=2, z=3) +``` + +You can exclude single attributes from certain methods: + +```{doctest} +>>> @define +... class C: +... user: str +... password: str = field(repr=False) +>>> C("me", "s3kr3t") +C(user='me') +``` + +Alternatively, to influence how the generated `__repr__()` method formats a specific attribute, specify a custom callable to be used instead of the `repr()` built-in function: + +```{doctest} +>>> @define +... class C: +... user: str +... password: str = field(repr=lambda value: '***') +>>> C("me", "s3kr3t") +C(user='me', password=***) +``` diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/examples.rst b/tests/wpt/tests/tools/third_party/attrs/docs/examples.rst deleted file mode 100644 index ba5343d4ad2..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/examples.rst +++ /dev/null @@ -1,709 +0,0 @@ -``attrs`` by Example -==================== - - -Basics ------- - -The simplest possible usage is: - -.. doctest:: - - >>> from attrs import define - >>> @define - ... class Empty: - ... pass - >>> Empty() - Empty() - >>> Empty() == Empty() - True - >>> Empty() is Empty() - False - -So in other words: ``attrs`` is useful even without actual attributes! - -But you'll usually want some data on your classes, so let's add some: - -.. doctest:: - - >>> @define - ... class Coordinates: - ... x: int - ... y: int - -By default, all features are added, so you immediately have a fully functional data class with a nice ``repr`` string and comparison methods. - -.. doctest:: - - >>> c1 = Coordinates(1, 2) - >>> c1 - Coordinates(x=1, y=2) - >>> c2 = Coordinates(x=2, y=1) - >>> c2 - Coordinates(x=2, y=1) - >>> c1 == c2 - False - -As shown, the generated ``__init__`` method allows for both positional and keyword arguments. - -For private attributes, ``attrs`` will strip the leading underscores for keyword arguments: - -.. doctest:: - - >>> @define - ... class C: - ... _x: int - >>> C(x=1) - C(_x=1) - -If you want to initialize your private attributes yourself, you can do that too: - -.. doctest:: - - >>> @define - ... class C: - ... _x: int = field(init=False, default=42) - >>> C() - C(_x=42) - >>> C(23) - Traceback (most recent call last): - ... - TypeError: __init__() takes exactly 1 argument (2 given) - -An additional way of defining attributes is supported too. -This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?): - -.. doctest:: - - >>> class SomethingFromSomeoneElse: - ... def __init__(self, x): - ... self.x = x - >>> SomethingFromSomeoneElse = define( - ... these={ - ... "x": field() - ... }, init=False)(SomethingFromSomeoneElse) - >>> SomethingFromSomeoneElse(1) - SomethingFromSomeoneElse(x=1) - - -`Subclassing is bad for you <https://www.youtube.com/watch?v=3MNVP9-hglc>`_, but ``attrs`` will still do what you'd hope for: - -.. doctest:: - - >>> @define(slots=False) - ... class A: - ... a: int - ... def get_a(self): - ... return self.a - >>> @define(slots=False) - ... class B: - ... b: int - >>> @define(slots=False) - ... class C(B, A): - ... c: int - >>> i = C(1, 2, 3) - >>> i - C(a=1, b=2, c=3) - >>> i == C(1, 2, 3) - True - >>> i.get_a() - 1 - -:term:`Slotted classes <slotted classes>`, which are the default for the new APIs, don't play well with multiple inheritance so we don't use them in the example. - -The order of the attributes is defined by the `MRO <https://www.python.org/download/releases/2.3/mro/>`_. - -Keyword-only Attributes -~~~~~~~~~~~~~~~~~~~~~~~ - -You can also add `keyword-only <https://docs.python.org/3/glossary.html#keyword-only-parameter>`_ attributes: - -.. doctest:: - - >>> @define - ... class A: - ... a: int = field(kw_only=True) - >>> A() - Traceback (most recent call last): - ... - TypeError: A() missing 1 required keyword-only argument: 'a' - >>> A(a=1) - A(a=1) - -``kw_only`` may also be specified at via ``define``, and will apply to all attributes: - -.. doctest:: - - >>> @define(kw_only=True) - ... class A: - ... a: int - ... b: int - >>> A(1, 2) - Traceback (most recent call last): - ... - TypeError: __init__() takes 1 positional argument but 3 were given - >>> A(a=1, b=2) - A(a=1, b=2) - - - -If you create an attribute with ``init=False``, the ``kw_only`` argument is ignored. - -Keyword-only attributes allow subclasses to add attributes without default values, even if the base class defines attributes with default values: - -.. doctest:: - - >>> @define - ... class A: - ... a: int = 0 - >>> @define - ... class B(A): - ... b: int = field(kw_only=True) - >>> B(b=1) - B(a=0, b=1) - >>> B() - Traceback (most recent call last): - ... - TypeError: B() missing 1 required keyword-only argument: 'b' - -If you don't set ``kw_only=True``, then there's is no valid attribute ordering and you'll get an error: - -.. doctest:: - - >>> @define - ... class A: - ... a: int = 0 - >>> @define - ... class B(A): - ... b: int - Traceback (most recent call last): - ... - ValueError: No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: Attribute(name='b', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, converter=None, metadata=mappingproxy({}), type=int, kw_only=False) - -.. _asdict: - -Converting to Collections Types -------------------------------- - -When you have a class with data, it often is very convenient to transform that class into a `dict` (for example if you want to serialize it to JSON): - -.. doctest:: - - >>> from attrs import asdict - - >>> asdict(Coordinates(x=1, y=2)) - {'x': 1, 'y': 2} - -Some fields cannot or should not be transformed. -For that, `attrs.asdict` offers a callback that decides whether an attribute should be included: - -.. doctest:: - - >>> @define - ... class User(object): - ... email: str - ... password: str - - >>> @define - ... class UserList: - ... users: list[User] - - >>> asdict(UserList([User("jane@doe.invalid", "s33kred"), - ... User("joe@doe.invalid", "p4ssw0rd")]), - ... filter=lambda attr, value: attr.name != "password") - {'users': [{'email': 'jane@doe.invalid'}, {'email': 'joe@doe.invalid'}]} - -For the common case where you want to `include <attr.filters.include>` or `exclude <attr.filters.exclude>` certain types or attributes, ``attrs`` ships with a few helpers: - -.. doctest:: - - >>> from attrs import asdict, filters, fields - - >>> @define - ... class User: - ... login: str - ... password: str - ... id: int - - >>> asdict( - ... User("jane", "s33kred", 42), - ... filter=filters.exclude(fields(User).password, int)) - {'login': 'jane'} - - >>> @define - ... class C: - ... x: str - ... y: str - ... z: int - - >>> asdict(C("foo", "2", 3), - ... filter=filters.include(int, fields(C).x)) - {'x': 'foo', 'z': 3} - -Other times, all you want is a tuple and ``attrs`` won't let you down: - -.. doctest:: - - >>> import sqlite3 - >>> from attrs import astuple - - >>> @define - ... class Foo: - ... a: int - ... b: int - - >>> foo = Foo(2, 3) - >>> with sqlite3.connect(":memory:") as conn: - ... c = conn.cursor() - ... c.execute("CREATE TABLE foo (x INTEGER PRIMARY KEY ASC, y)") #doctest: +ELLIPSIS - ... c.execute("INSERT INTO foo VALUES (?, ?)", astuple(foo)) #doctest: +ELLIPSIS - ... foo2 = Foo(*c.execute("SELECT x, y FROM foo").fetchone()) - <sqlite3.Cursor object at ...> - <sqlite3.Cursor object at ...> - >>> foo == foo2 - True - -For more advanced transformations and conversions, we recommend you look at a companion library (such as `cattrs <https://github.com/python-attrs/cattrs>`_). - -Defaults --------- - -Sometimes you want to have default values for your initializer. -And sometimes you even want mutable objects as default values (ever accidentally used ``def f(arg=[])``?). -``attrs`` has you covered in both cases: - -.. doctest:: - - >>> import collections - - >>> @define - ... class Connection: - ... socket: int - ... @classmethod - ... def connect(cls, db_string): - ... # ... connect somehow to db_string ... - ... return cls(socket=42) - - >>> @define - ... class ConnectionPool: - ... db_string: str - ... pool: collections.deque = Factory(collections.deque) - ... debug: bool = False - ... def get_connection(self): - ... try: - ... return self.pool.pop() - ... except IndexError: - ... if self.debug: - ... print("New connection!") - ... return Connection.connect(self.db_string) - ... def free_connection(self, conn): - ... if self.debug: - ... print("Connection returned!") - ... self.pool.appendleft(conn) - ... - >>> cp = ConnectionPool("postgres://localhost") - >>> cp - ConnectionPool(db_string='postgres://localhost', pool=deque([]), debug=False) - >>> conn = cp.get_connection() - >>> conn - Connection(socket=42) - >>> cp.free_connection(conn) - >>> cp - ConnectionPool(db_string='postgres://localhost', pool=deque([Connection(socket=42)]), debug=False) - -More information on why class methods for constructing objects are awesome can be found in this insightful `blog post <https://web.archive.org/web/20210130220433/http://as.ynchrono.us/2014/12/asynchronous-object-initialization.html>`_. - -Default factories can also be set using the ``factory`` argument to ``field``, and using a decorator. -The method receives the partially initialized instance which enables you to base a default value on other attributes: - -.. doctest:: - - >>> @define - ... class C: - ... x: int = 1 - ... y: int = field() - ... @y.default - ... def _any_name_except_a_name_of_an_attribute(self): - ... return self.x + 1 - ... z: list = field(factory=list) - >>> C() - C(x=1, y=2, z=[]) - - -.. _examples_validators: - -Validators ----------- - -Although your initializers should do as little as possible (ideally: just initialize your instance according to the arguments!), it can come in handy to do some kind of validation on the arguments. - -``attrs`` offers two ways to define validators for each attribute and it's up to you to choose which one suits your style and project better. - -You can use a decorator: - -.. doctest:: - - >>> @define - ... class C: - ... x: int = field() - ... @x.validator - ... def check(self, attribute, value): - ... if value > 42: - ... raise ValueError("x must be smaller or equal to 42") - >>> C(42) - C(x=42) - >>> C(43) - Traceback (most recent call last): - ... - ValueError: x must be smaller or equal to 42 - -...or a callable... - -.. doctest:: - - >>> from attrs import validators - - >>> def x_smaller_than_y(instance, attribute, value): - ... if value >= instance.y: - ... raise ValueError("'x' has to be smaller than 'y'!") - >>> @define - ... class C: - ... x: int = field(validator=[validators.instance_of(int), - ... x_smaller_than_y]) - ... y: int - >>> C(x=3, y=4) - C(x=3, y=4) - >>> C(x=4, y=3) - Traceback (most recent call last): - ... - ValueError: 'x' has to be smaller than 'y'! - -...or both at once: - -.. doctest:: - - >>> @define - ... class C: - ... x: int = field(validator=validators.instance_of(int)) - ... @x.validator - ... def fits_byte(self, attribute, value): - ... if not 0 <= value < 256: - ... raise ValueError("value out of bounds") - >>> C(128) - C(x=128) - >>> C("128") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=int, converter=None, kw_only=False), <class 'int'>, '128') - >>> C(256) - Traceback (most recent call last): - ... - ValueError: value out of bounds - -Please note that the decorator approach only works if -- and only if! -- the attribute in question has a ``field`` assigned. -Therefore if you use ``@default``, it is *not* enough to annotate said attribute with a type. - -``attrs`` ships with a bunch of validators, make sure to `check them out <api_validators>` before writing your own: - -.. doctest:: - - >>> @define - ... class C: - ... x: int = field(validator=validators.instance_of(int)) - >>> C(42) - C(x=42) - >>> C("42") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None, kw_only=False), <type 'int'>, '42') - -Please note that if you use `attr.s` (and not `attrs.define`) to define your class, validators only run on initialization by default. -This behavior can be changed using the ``on_setattr`` argument. - -Check out `validators` for more details. - - -Conversion ----------- - -Attributes can have a ``converter`` function specified, which will be called with the attribute's passed-in value to get a new value to use. -This can be useful for doing type-conversions on values that you don't want to force your callers to do. - -.. doctest:: - - >>> @define - ... class C: - ... x: int = field(converter=int) - >>> o = C("1") - >>> o.x - 1 - -Please note that converters only run on initialization. - -Check out `converters` for more details. - - -.. _metadata: - -Metadata --------- - -All ``attrs`` attributes may include arbitrary metadata in the form of a read-only dictionary. - -.. doctest:: - - >>> from attrs import fields - - >>> @define - ... class C: - ... x = field(metadata={'my_metadata': 1}) - >>> fields(C).x.metadata - mappingproxy({'my_metadata': 1}) - >>> fields(C).x.metadata['my_metadata'] - 1 - -Metadata is not used by ``attrs``, and is meant to enable rich functionality in third-party libraries. -The metadata dictionary follows the normal dictionary rules: keys need to be hashable, and both keys and values are recommended to be immutable. - -If you're the author of a third-party library with ``attrs`` integration, please see `Extending Metadata <extending_metadata>`. - - -Types ------ - -``attrs`` also allows you to associate a type with an attribute using either the *type* argument to `attr.ib` or -- as of Python 3.6 -- using `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_-annotations: - - -.. doctest:: - - >>> from attrs import fields - - >>> @define - ... class C: - ... x: int - >>> fields(C).x.type - <class 'int'> - - >>> import attr - >>> @attr.s - ... class C(object): - ... x = attr.ib(type=int) - >>> fields(C).x.type - <class 'int'> - -If you don't mind annotating *all* attributes, you can even drop the `attrs.field` and assign default values instead: - -.. doctest:: - - >>> import typing - >>> from attrs import fields - - >>> @define - ... class AutoC: - ... cls_var: typing.ClassVar[int] = 5 # this one is ignored - ... l: list[int] = Factory(list) - ... x: int = 1 - ... foo: str = "every attrib needs a type if auto_attribs=True" - ... bar: typing.Any = None - >>> fields(AutoC).l.type - list[int] - >>> fields(AutoC).x.type - <class 'int'> - >>> fields(AutoC).foo.type - <class 'str'> - >>> fields(AutoC).bar.type - typing.Any - >>> AutoC() - AutoC(l=[], x=1, foo='every attrib needs a type if auto_attribs=True', bar=None) - >>> AutoC.cls_var - 5 - -The generated ``__init__`` method will have an attribute called ``__annotations__`` that contains this type information. - -If your annotations contain strings (e.g. forward references), -you can resolve these after all references have been defined by using :func:`attrs.resolve_types`. -This will replace the *type* attribute in the respective fields. - -.. doctest:: - - >>> from attrs import fields, resolve_types - - >>> @define - ... class A: - ... a: 'list[A]' - ... b: 'B' - ... - >>> @define - ... class B: - ... a: A - ... - >>> fields(A).a.type - 'list[A]' - >>> fields(A).b.type - 'B' - >>> resolve_types(A, globals(), locals()) - <class 'A'> - >>> fields(A).a.type - list[A] - >>> fields(A).b.type - <class 'B'> - -.. note:: - - If you find yourself using string type annotations to handle forward references, wrap the entire type annotation in quotes instead of only the type you need a forward reference to (so ``'list[A]'`` instead of ``list['A']``). - This is a limitation of the Python typing system. - -.. warning:: - - ``attrs`` itself doesn't have any features that work on top of type metadata *yet*. - However it's useful for writing your own validators or serialization frameworks. - - -Slots ------ - -:term:`Slotted classes <slotted classes>` have several advantages on CPython. -Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `attrs.define` or passing ``slots=True`` to `attr.s`: - -.. doctest:: - - >>> import attr - - >>> @attr.s(slots=True) - ... class Coordinates: - ... x: int - ... y: int - - -Immutability ------------- - -Sometimes you have instances that shouldn't be changed after instantiation. -Immutability is especially popular in functional programming and is generally a very good thing. -If you'd like to enforce it, ``attrs`` will try to help: - -.. doctest:: - - >>> @frozen - ... class C: - ... x: int - >>> i = C(1) - >>> i.x = 2 - Traceback (most recent call last): - ... - attr.exceptions.FrozenInstanceError: can't set attribute - >>> i.x - 1 - -Please note that true immutability is impossible in Python but it will `get <how-frozen>` you 99% there. -By themselves, immutable classes are useful for long-lived objects that should never change; like configurations for example. - -In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes. -In Clojure that function is called `assoc <https://clojuredocs.org/clojure.core/assoc>`_ and ``attrs`` shamelessly imitates it: `attr.evolve`: - -.. doctest:: - - >>> from attrs import evolve - - >>> @frozen - ... class C: - ... x: int - ... y: int - >>> i1 = C(1, 2) - >>> i1 - C(x=1, y=2) - >>> i2 = evolve(i1, y=3) - >>> i2 - C(x=1, y=3) - >>> i1 == i2 - False - - -Other Goodies -------------- - -Sometimes you may want to create a class programmatically. -``attrs`` won't let you down and gives you `attrs.make_class` : - -.. doctest:: - - >>> from attrs import fields, make_class - >>> @define - ... class C1: - ... x = field() - ... y = field() - >>> C2 = make_class("C2", ["x", "y"]) - >>> fields(C1) == fields(C2) - True - -You can still have power over the attributes if you pass a dictionary of name: ``field`` mappings and can pass arguments to ``@attr.s``: - -.. doctest:: - - >>> from attrs import make_class - - >>> C = make_class("C", {"x": field(default=42), - ... "y": field(default=Factory(list))}, - ... repr=False) - >>> i = C() - >>> i # no repr added! - <__main__.C object at ...> - >>> i.x - 42 - >>> i.y - [] - -If you need to dynamically make a class with `attrs.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: - -.. doctest:: - - >>> from attrs import make_class - - >>> class D: - ... def __eq__(self, other): - ... return True # arbitrary example - >>> C = make_class("C", {}, bases=(D,), cmp=False) - >>> isinstance(C(), D) - True - -Sometimes, you want to have your class's ``__init__`` method do more than just -the initialization, validation, etc. that gets done for you automatically when -using ``@define``. -To do this, just define a ``__attrs_post_init__`` method in your class. -It will get called at the end of the generated ``__init__`` method. - -.. doctest:: - - >>> @define - ... class C: - ... x: int - ... y: int - ... z: int = field(init=False) - ... - ... def __attrs_post_init__(self): - ... self.z = self.x + self.y - >>> obj = C(x=1, y=2) - >>> obj - C(x=1, y=2, z=3) - -You can exclude single attributes from certain methods: - -.. doctest:: - - >>> @define - ... class C: - ... user: str - ... password: str = field(repr=False) - >>> C("me", "s3kr3t") - C(user='me') - -Alternatively, to influence how the generated ``__repr__()`` method formats a specific attribute, specify a custom callable to be used instead of the ``repr()`` built-in function: - -.. doctest:: - - >>> @define - ... class C: - ... user: str - ... password: str = field(repr=lambda value: '***') - >>> C("me", "s3kr3t") - C(user='me', password=***) diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/extending.md b/tests/wpt/tests/tools/third_party/attrs/docs/extending.md new file mode 100644 index 00000000000..c6cb5f574bc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/extending.md @@ -0,0 +1,314 @@ +# Extending + +Each *attrs*-decorated class has a `__attrs_attrs__` class attribute. +It's a tuple of {class}`attrs.Attribute` carrying metadata about each attribute. + +So it is fairly simple to build your own decorators on top of *attrs*: + +```{doctest} +>>> from attr import define +>>> def print_attrs(cls): +... print(cls.__attrs_attrs__) +... return cls +>>> @print_attrs +... @define +... class C: +... a: int +(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),) +``` + +:::{warning} +The {func}`attrs.define` / {func}`attr.s` decorator **must** be applied first because it puts `__attrs_attrs__` in place! +That means that is has to come *after* your decorator because: + +```python +@a +@b +def f(): + pass +``` + +is just [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) for: + +```python +def original_f(): + pass + +f = a(b(original_f)) +``` +::: + + +## Wrapping the Decorator + +A more elegant way can be to wrap *attrs* altogether and build a class [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) on top of it. + +An example for that is the package [*environ-config*](https://github.com/hynek/environ-config) that uses *attrs* under the hood to define environment-based configurations declaratively without exposing *attrs* APIs at all. + +Another common use case is to overwrite *attrs*'s defaults. + + +### Mypy + +Unfortunately, decorator wrapping currently [confuses](https://github.com/python/mypy/issues/5406) mypy's *attrs* plugin. +At the moment, the best workaround is to hold your nose, write a fake *Mypy* plugin, and mutate a bunch of global variables: + +```python +from mypy.plugin import Plugin +from mypy.plugins.attrs import ( + attr_attrib_makers, + attr_class_makers, + attr_dataclass_makers, +) + +# These work just like `attr.dataclass`. +attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass") + +# This works just like `attr.s`. +attr_class_makers.add("my_module.method_looks_like_attr_s") + +# These are our `attr.ib` makers. +attr_attrib_makers.add("my_module.method_looks_like_attrib") + +class MyPlugin(Plugin): + # Our plugin does nothing but it has to exist so this file gets loaded. + pass + + +def plugin(version): + return MyPlugin +``` + +Then tell *Mypy* about your plugin using your project's `mypy.ini`: + +```ini +[mypy] +plugins=<path to file> +``` + +:::{warning} +Please note that it is currently *impossible* to let mypy know that you've changed defaults like *eq* or *order*. +You can only use this trick to tell *Mypy* that a class is actually an *attrs* class. +::: + + +### Pyright + +Generic decorator wrapping is supported in [*Pyright*](https://github.com/microsoft/pyright) via `typing.dataclass_transform` / {pep}`689`. + +For a custom wrapping of the form: + +``` +@typing.dataclass_transform(field_specifiers=(attr.attrib, attr.field)) +def custom_define(f): + return attr.define(f) +``` + +## Types + +*attrs* offers two ways of attaching type information to attributes: + +- {pep}`526` annotations, +- and the *type* argument to {func}`attr.ib`. + +This information is available to you: + +```{doctest} +>>> from attr import attrib, define, field, fields +>>> @define +... class C: +... x: int = field() +... y = attrib(type=str) +>>> fields(C).x.type +<class 'int'> +>>> fields(C).y.type +<class 'str'> +``` + +Currently, *attrs* doesn't do anything with this information but it's very useful if you'd like to write your own validators or serializers! + +(extending-metadata)= + +## Metadata + +If you're the author of a third-party library with *attrs* integration, you may want to take advantage of attribute metadata. + +Here are some tips for effective use of metadata: + +- Try making your metadata keys and values immutable. + This keeps the entire {class}`~attrs.Attribute` instances immutable too. + +- To avoid metadata key collisions, consider exposing your metadata keys from your modules.: + + ```python + from mylib import MY_METADATA_KEY + + @define + class C: + x = field(metadata={MY_METADATA_KEY: 1}) + ``` + + Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways. + +- Expose `field` wrappers for your specific metadata. + This is a more graceful approach if your users don't require metadata from other libraries. + + ```{doctest} + >>> from attrs import fields, NOTHING + >>> MY_TYPE_METADATA = '__my_type_metadata' + >>> + >>> def typed( + ... cls, default=NOTHING, validator=None, repr=True, + ... eq=True, order=None, hash=None, init=True, metadata=None, + ... converter=None + ... ): + ... metadata = metadata or {} + ... metadata[MY_TYPE_METADATA] = cls + ... return field( + ... default=default, validator=validator, repr=repr, + ... eq=eq, order=order, hash=hash, init=init, + ... metadata=metadata, converter=converter + ... ) + >>> + >>> @define + ... class C: + ... x: int = typed(int, default=1, init=False) + >>> fields(C).x.metadata[MY_TYPE_METADATA] + <class 'int'> + ``` + +(transform-fields)= + +## Automatic Field Transformation and Modification + +*attrs* allows you to automatically modify or transform the class' fields while the class is being created. +You do this by passing a *field_transformer* hook to {func}`~attrs.define` (and friends). +Its main purpose is to automatically add converters to attributes based on their type to aid the development of API clients and other typed data loaders. + +This hook must have the following signature: + +```{eval-rst} +.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute] + :noindex: +``` + +- *cls* is your class right *before* it is being converted into an attrs class. + This means it does not yet have the `__attrs_attrs__` attribute. +- *fields* is a list of all `attrs.Attribute` instances that will later be set to `__attrs_attrs__`. + You can modify these attributes any way you want: + You can add converters, change types, and even remove attributes completely or create new ones! + +For example, let's assume that you really don't like floats: + +```{doctest} +>>> def drop_floats(cls, fields): +... return [f for f in fields if f.type not in {float, 'float'}] +... +>>> @frozen(field_transformer=drop_floats) +... class Data: +... a: int +... b: float +... c: str +... +>>> Data(42, "spam") +Data(a=42, c='spam') +``` + +A more realistic example would be to automatically convert data that you, e.g., load from JSON: + +```{doctest} +>>> from datetime import datetime +>>> +>>> def auto_convert(cls, fields): +... results = [] +... for field in fields: +... if field.converter is not None: +... results.append(field) +... continue +... if field.type in {datetime, 'datetime'}: +... converter = (lambda d: datetime.fromisoformat(d) if isinstance(d, str) else d) +... else: +... converter = None +... results.append(field.evolve(converter=converter)) +... return results +... +>>> @frozen(field_transformer=auto_convert) +... class Data: +... a: int +... b: str +... c: datetime +... +>>> from_json = {"a": 3, "b": "spam", "c": "2020-05-04T13:37:00"} +>>> Data(**from_json) # **** +Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37)) +``` + +Or, perhaps you would prefer to generate dataclass-compatible `__init__` signatures via a default field *alias*. +Note, *field_transformer* operates on {class}`attrs.Attribute` instances before the default private-attribute handling is applied so explicit user-provided aliases can be detected. + +```{doctest} +>>> def dataclass_names(cls, fields): +... return [ +... field.evolve(alias=field.name) +... if not field.alias +... else field +... for field in fields +... ] +... +>>> @frozen(field_transformer=dataclass_names) +... class Data: +... public: int +... _private: str +... explicit: str = field(alias="aliased_name") +... +>>> Data(public=42, _private="spam", aliased_name="yes") +Data(public=42, _private='spam', explicit='yes') +``` + +## Customize Value Serialization in `asdict()` + +*attrs* allows you to serialize instances of *attrs* classes to dicts using the {func}`attrs.asdict` function. +However, the result can not always be serialized since most data types will remain as they are: + +```{doctest} +>>> import json +>>> import datetime +>>> from attrs import asdict +>>> +>>> @frozen +... class Data: +... dt: datetime.datetime +... +>>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) +>>> data +{'dt': datetime.datetime(2020, 5, 4, 13, 37)} +>>> json.dumps(data) +Traceback (most recent call last): + ... +TypeError: Object of type datetime is not JSON serializable +``` + +To help you with this, {func}`~attrs.asdict` allows you to pass a *value_serializer* hook. +It has the signature + +```{eval-rst} +.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any + :noindex: +``` + +```{doctest} +>>> from attr import asdict +>>> def serialize(inst, field, value): +... if isinstance(value, datetime.datetime): +... return value.isoformat() +... return value +... +>>> data = asdict( +... Data(datetime.datetime(2020, 5, 4, 13, 37)), +... value_serializer=serialize, +... ) +>>> data +{'dt': '2020-05-04T13:37:00'} +>>> json.dumps(data) +'{"dt": "2020-05-04T13:37:00"}' +``` diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/extending.rst b/tests/wpt/tests/tools/third_party/attrs/docs/extending.rst deleted file mode 100644 index faf71afd913..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/extending.rst +++ /dev/null @@ -1,313 +0,0 @@ -Extending -========= - -Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. -It's a tuple of `attrs.Attribute` carrying metadata about each attribute. - -So it is fairly simple to build your own decorators on top of ``attrs``: - -.. doctest:: - - >>> from attr import define - >>> def print_attrs(cls): - ... print(cls.__attrs_attrs__) - ... return cls - >>> @print_attrs - ... @define - ... class C: - ... a: int - (Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None),) - - -.. warning:: - - The `attrs.define`/`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! - That means that is has to come *after* your decorator because:: - - @a - @b - def f(): - pass - - is just `syntactic sugar <https://en.wikipedia.org/wiki/Syntactic_sugar>`_ for:: - - def original_f(): - pass - - f = a(b(original_f)) - - -Wrapping the Decorator ----------------------- - -A more elegant way can be to wrap ``attrs`` altogether and build a class `DSL <https://en.wikipedia.org/wiki/Domain-specific_language>`_ on top of it. - -An example for that is the package `environ-config <https://github.com/hynek/environ-config>`_ that uses ``attrs`` under the hood to define environment-based configurations declaratively without exposing ``attrs`` APIs at all. - -Another common use case is to overwrite ``attrs``'s defaults. - -Mypy -^^^^ - -Unfortunately, decorator wrapping currently `confuses <https://github.com/python/mypy/issues/5406>`_ mypy's ``attrs`` plugin. -At the moment, the best workaround is to hold your nose, write a fake mypy plugin, and mutate a bunch of global variables:: - - from mypy.plugin import Plugin - from mypy.plugins.attrs import ( - attr_attrib_makers, - attr_class_makers, - attr_dataclass_makers, - ) - - # These work just like `attr.dataclass`. - attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass") - - # This works just like `attr.s`. - attr_class_makers.add("my_module.method_looks_like_attr_s") - - # These are our `attr.ib` makers. - attr_attrib_makers.add("my_module.method_looks_like_attrib") - - class MyPlugin(Plugin): - # Our plugin does nothing but it has to exist so this file gets loaded. - pass - - - def plugin(version): - return MyPlugin - - -Then tell mypy about your plugin using your project's ``mypy.ini``: - -.. code:: ini - - [mypy] - plugins=<path to file> - - -.. warning:: - Please note that it is currently *impossible* to let mypy know that you've changed defaults like *eq* or *order*. - You can only use this trick to tell mypy that a class is actually an ``attrs`` class. - -Pyright -^^^^^^^ - -Generic decorator wrapping is supported in `pyright <https://github.com/microsoft/pyright>`_ via their dataclass_transform_ specification. - -For a custom wrapping of the form:: - - def custom_define(f): - return attr.define(f) - -This is implemented via a ``__dataclass_transform__`` type decorator in the custom extension's ``.pyi`` of the form:: - - def __dataclass_transform__( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), - ) -> Callable[[_T], _T]: ... - - @__dataclass_transform__(field_descriptors=(attr.attrib, attr.field)) - def custom_define(f): ... - -.. warning:: - - ``dataclass_transform`` is supported **provisionally** as of ``pyright`` 1.1.135. - - Both the ``pyright`` dataclass_transform_ specification and ``attrs`` implementation may change in future versions. - - -Types ------ - -``attrs`` offers two ways of attaching type information to attributes: - -- `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_ annotations on Python 3.6 and later, -- and the *type* argument to `attr.ib`. - -This information is available to you: - -.. doctest:: - - >>> from attr import attrib, define, field, fields - >>> @define - ... class C: - ... x: int = field() - ... y = attrib(type=str) - >>> fields(C).x.type - <class 'int'> - >>> fields(C).y.type - <class 'str'> - -Currently, ``attrs`` doesn't do anything with this information but it's very useful if you'd like to write your own validators or serializers! - - -.. _extending_metadata: - -Metadata --------- - -If you're the author of a third-party library with ``attrs`` integration, you may want to take advantage of attribute metadata. - -Here are some tips for effective use of metadata: - -- Try making your metadata keys and values immutable. - This keeps the entire ``Attribute`` instances immutable too. - -- To avoid metadata key collisions, consider exposing your metadata keys from your modules.:: - - from mylib import MY_METADATA_KEY - - @define - class C: - x = field(metadata={MY_METADATA_KEY: 1}) - - Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways. - -- Expose ``field`` wrappers for your specific metadata. - This is a more graceful approach if your users don't require metadata from other libraries. - - .. doctest:: - - >>> from attr import fields, NOTHING - >>> MY_TYPE_METADATA = '__my_type_metadata' - >>> - >>> def typed( - ... cls, default=NOTHING, validator=None, repr=True, - ... eq=True, order=None, hash=None, init=True, metadata={}, - ... converter=None - ... ): - ... metadata = dict() if not metadata else metadata - ... metadata[MY_TYPE_METADATA] = cls - ... return field( - ... default=default, validator=validator, repr=repr, - ... eq=eq, order=order, hash=hash, init=init, - ... metadata=metadata, converter=converter - ... ) - >>> - >>> @define - ... class C: - ... x: int = typed(int, default=1, init=False) - >>> fields(C).x.metadata[MY_TYPE_METADATA] - <class 'int'> - - -.. _transform-fields: - -Automatic Field Transformation and Modification ------------------------------------------------ - -``attrs`` allows you to automatically modify or transform the class' fields while the class is being created. -You do this by passing a *field_transformer* hook to `attr.define` (and its friends). -Its main purpose is to automatically add converters to attributes based on their type to aid the development of API clients and other typed data loaders. - -This hook must have the following signature: - -.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute] - :noindex: - -- *cls* is your class right *before* it is being converted into an attrs class. - This means it does not yet have the ``__attrs_attrs__`` attribute. - -- *fields* is a list of all `attrs.Attribute` instances that will later be set to ``__attrs_attrs__``. - You can modify these attributes any way you want: - You can add converters, change types, and even remove attributes completely or create new ones! - -For example, let's assume that you really don't like floats: - -.. doctest:: - - >>> def drop_floats(cls, fields): - ... return [f for f in fields if f.type not in {float, 'float'}] - ... - >>> @frozen(field_transformer=drop_floats) - ... class Data: - ... a: int - ... b: float - ... c: str - ... - >>> Data(42, "spam") - Data(a=42, c='spam') - -A more realistic example would be to automatically convert data that you, e.g., load from JSON: - -.. doctest:: - - >>> from datetime import datetime - >>> - >>> def auto_convert(cls, fields): - ... results = [] - ... for field in fields: - ... if field.converter is not None: - ... results.append(field) - ... continue - ... if field.type in {datetime, 'datetime'}: - ... converter = (lambda d: datetime.fromisoformat(d) if isinstance(d, str) else d) - ... else: - ... converter = None - ... results.append(field.evolve(converter=converter)) - ... return results - ... - >>> @frozen(field_transformer=auto_convert) - ... class Data: - ... a: int - ... b: str - ... c: datetime - ... - >>> from_json = {"a": 3, "b": "spam", "c": "2020-05-04T13:37:00"} - >>> Data(**from_json) # **** - Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37)) - - -Customize Value Serialization in ``asdict()`` ---------------------------------------------- - -``attrs`` allows you to serialize instances of ``attrs`` classes to dicts using the `attrs.asdict` function. -However, the result can not always be serialized since most data types will remain as they are: - -.. doctest:: - - >>> import json - >>> import datetime - >>> from attrs import asdict - >>> - >>> @frozen - ... class Data: - ... dt: datetime.datetime - ... - >>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) - >>> data - {'dt': datetime.datetime(2020, 5, 4, 13, 37)} - >>> json.dumps(data) - Traceback (most recent call last): - ... - TypeError: Object of type datetime is not JSON serializable - -To help you with this, `attr.asdict` allows you to pass a *value_serializer* hook. -It has the signature - -.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any - :noindex: - -.. doctest:: - - >>> from attr import asdict - >>> def serialize(inst, field, value): - ... if isinstance(value, datetime.datetime): - ... return value.isoformat() - ... return value - ... - >>> data = asdict( - ... Data(datetime.datetime(2020, 5, 4, 13, 37)), - ... value_serializer=serialize, - ... ) - >>> data - {'dt': '2020-05-04T13:37:00'} - >>> json.dumps(data) - '{"dt": "2020-05-04T13:37:00"}' - -***** - -.. _dataclass_transform: https://github.com/microsoft/pyright/blob/master/specs/dataclass_transforms.md diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/glossary.md b/tests/wpt/tests/tools/third_party/attrs/docs/glossary.md new file mode 100644 index 00000000000..6b09a3ad4d1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/glossary.md @@ -0,0 +1,108 @@ +# Glossary + +:::{glossary} +dunder methods + "Dunder" is a contraction of "double underscore". + + It's methods like `__init__` or `__eq__` that are sometimes also called *magic methods* or it's said that they implement an *object protocol*. + + In spoken form, you'd call `__init__` just "dunder init". + + Its first documented use is a [mailing list posting](https://mail.python.org/pipermail/python-list/2002-September/155836.html) by Mark Jackson from 2002. + +dict classes + A regular class whose attributes are stored in the {attr}`object.__dict__` attribute of every single instance. + This is quite wasteful especially for objects with very few data attributes and the space consumption can become significant when creating large numbers of instances. + + This is the type of class you get by default both with and without *attrs* (except with the next APIs {func}`attrs.define()`, [`attrs.mutable()`](attrs.mutable), and [`attrs.frozen()`](attrs.frozen)). + +slotted classes + A class whose instances have no {attr}`object.__dict__` attribute and [define](https://docs.python.org/3/reference/datamodel.html#slots) their attributes in a `object.__slots__` attribute instead. + In *attrs*, they are created by passing `slots=True` to `@attr.s` (and are on by default in {func}`attrs.define()`, [`attrs.mutable()`](attrs.mutable), and [`attrs.frozen()`](attrs.frozen)). + + Their main advantage is that they use less memory on CPython[^pypy] and are slightly faster. + + However, they also come with several possibly surprising gotchas: + + - Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies `__slots__`: + + ```{doctest} + >>> from attr import define + >>> @define + ... class Coordinates: + ... x: int + ... y: int + ... + >>> c = Coordinates(x=1, y=2) + >>> c.z = 3 + Traceback (most recent call last): + ... + AttributeError: 'Coordinates' object has no attribute 'z' + ``` + + - Slotted classes can inherit from other classes just like non-slotted classes, but some of the benefits of slotted classes are lost if you do that. + If you must inherit from other classes, try to inherit only from other slotted classes. + + - However, [it's not possible](https://docs.python.org/3/reference/datamodel.html#slots) to inherit from more than one class that has attributes in `__slots__` (you will get an `TypeError: multiple bases have instance lay-out conflict`). + + - It's not possible to monkeypatch methods on slotted classes. + This can feel limiting in test code, however the need to monkeypatch your own classes is usually a design smell. + + If you really need to monkeypatch an instance in your tests, but don't want to give up on the advantages of slotted classes in production code, you can always subclass a slotted class as a dict class with no further changes and all the limitations go away: + + ```{doctest} + >>> import unittest.mock + >>> @define + ... class Slotted: + ... x: int + ... + ... def method(self): + ... return self.x + >>> s = Slotted(42) + >>> s.method() + 42 + >>> with unittest.mock.patch.object(s, "method", return_value=23): + ... pass + Traceback (most recent call last): + ... + AttributeError: 'Slotted' object attribute 'method' is read-only + >>> @define(slots=False) + ... class Dicted(Slotted): + ... pass + >>> d = Dicted(42) + >>> d.method() + 42 + >>> with unittest.mock.patch.object(d, "method", return_value=23): + ... assert 23 == d.method() + ``` + + - Slotted classes must implement {meth}`__getstate__ <object.__getstate__>` and {meth}`__setstate__ <object.__setstate__>` to be serializable with {mod}`pickle` protocol 0 and 1. + Therefore, *attrs* creates these methods automatically for slotted classes. + + :::{note} + When decorating with `@attr.s(slots=True)` and the class already implements the {meth}`__getstate__ <object.__getstate__>` and {meth}`__setstate__ <object.__setstate__>` methods, they will be *overwritten* by *attrs* autogenerated implementation by default. + + This can be avoided by setting `@attr.s(getstate_setstate=False)` or by setting `@attr.s(auto_detect=True)`. + + {func}`~attrs.define` sets `auto_detect=True` by default. + ::: + + Also, [think twice](https://www.youtube.com/watch?v=7KnfGDajDQw) before using {mod}`pickle`. + + - Slotted classes are weak-referenceable by default. + This can be disabled in CPython by passing `weakref_slot=False` to `@attr.s` [^pypyweakref]. + + - Since it's currently impossible to make a class slotted after it's been created, *attrs* has to replace your class with a new one. + While it tries to do that as graciously as possible, certain metaclass features like {meth}`object.__init_subclass__` do not work with slotted classes. + + - The {attr}`class.__subclasses__` attribute needs a garbage collection run (which can be manually triggered using {func}`gc.collect`), for the original class to be removed. + See issue [#407](https://github.com/python-attrs/attrs/issues/407) for more details. + + - Pickling of slotted classes will fail if you define a class with missing attributes. + + This situation can occur if you define an `attrs.field(init=False)` and don't set the attribute by hand before pickling. +::: + +[^pypy]: On PyPy, there is no memory advantage in using slotted classes. + +[^pypyweakref]: On PyPy, slotted classes are naturally weak-referenceable so `weakref_slot=False` has no effect. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/glossary.rst b/tests/wpt/tests/tools/third_party/attrs/docs/glossary.rst deleted file mode 100644 index 5fd01f4fb19..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/glossary.rst +++ /dev/null @@ -1,104 +0,0 @@ -Glossary -======== - -.. glossary:: - - dunder methods - "Dunder" is a contraction of "double underscore". - - It's methods like ``__init__`` or ``__eq__`` that are sometimes also called *magic methods* or it's said that they implement an *object protocol*. - - In spoken form, you'd call ``__init__`` just "dunder init". - - Its first documented use is a `mailing list posting <https://mail.python.org/pipermail/python-list/2002-September/155836.html>`_ by Mark Jackson from 2002. - - dict classes - A regular class whose attributes are stored in the `object.__dict__` attribute of every single instance. - This is quite wasteful especially for objects with very few data attributes and the space consumption can become significant when creating large numbers of instances. - - This is the type of class you get by default both with and without ``attrs`` (except with the next APIs `attr.define`, `attr.mutable`, and `attr.frozen`). - - slotted classes - A class whose instances have no `object.__dict__` attribute and `define <https://docs.python.org/3/reference/datamodel.html#slots>`_ their attributes in a `object.__slots__` attribute instead. - In ``attrs``, they are created by passing ``slots=True`` to ``@attr.s`` (and are on by default in `attr.define`/`attr.mutable`/`attr.frozen`). - - - Their main advantage is that they use less memory on CPython [#pypy]_ and are slightly faster. - - However they also come with several possibly surprising gotchas: - - - Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies ``__slots__``: - - .. doctest:: - - >>> from attr import define - >>> @define - ... class Coordinates: - ... x: int - ... y: int - ... - >>> c = Coordinates(x=1, y=2) - >>> c.z = 3 - Traceback (most recent call last): - ... - AttributeError: 'Coordinates' object has no attribute 'z' - - - Slotted classes can inherit from other classes just like non-slotted classes, but some of the benefits of slotted classes are lost if you do that. - If you must inherit from other classes, try to inherit only from other slotted classes. - - - However, `it's not possible <https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots>`_ to inherit from more than one class that has attributes in ``__slots__`` (you will get an ``TypeError: multiple bases have instance lay-out conflict``). - - - It's not possible to monkeypatch methods on slotted classes. - This can feel limiting in test code, however the need to monkeypatch your own classes is usually a design smell. - - If you really need to monkeypatch an instance in your tests, but don't want to give up on the advantages of slotted classes in production code, you can always subclass a slotted class as a dict class with no further changes and all the limitations go away: - - .. doctest:: - - >>> import attr, unittest.mock - >>> @define - ... class Slotted: - ... x: int - ... - ... def method(self): - ... return self.x - >>> s = Slotted(42) - >>> s.method() - 42 - >>> with unittest.mock.patch.object(s, "method", return_value=23): - ... pass - Traceback (most recent call last): - ... - AttributeError: 'Slotted' object attribute 'method' is read-only - >>> @define(slots=False) - ... class Dicted(Slotted): - ... pass - >>> d = Dicted(42) - >>> d.method() - 42 - >>> with unittest.mock.patch.object(d, "method", return_value=23): - ... assert 23 == d.method() - - - Slotted classes must implement :meth:`__getstate__ <object.__getstate__>` and :meth:`__setstate__ <object.__setstate__>` to be serializable with `pickle` protocol 0 and 1. - Therefore, ``attrs`` creates these methods automatically for ``slots=True`` classes (Python 2 uses protocol 0 by default). - - .. note:: - - If the ``@attr.s(slots=True)`` decorated class already implements the :meth:`__getstate__ <object.__getstate__>` and :meth:`__setstate__ <object.__setstate__>` methods, they will be *overwritten* by ``attrs`` autogenerated implementation by default. - - This can be avoided by setting ``@attr.s(getstate_setstate=False)`` or by setting ``@attr.s(auto_detect=True)``. - - Also, `think twice <https://www.youtube.com/watch?v=7KnfGDajDQw>`_ before using `pickle`. - - - Slotted classes are weak-referenceable by default. - This can be disabled in CPython by passing ``weakref_slot=False`` to ``@attr.s`` [#pypyweakref]_. - - - Since it's currently impossible to make a class slotted after it's been created, ``attrs`` has to replace your class with a new one. - While it tries to do that as graciously as possible, certain metaclass features like `object.__init_subclass__` do not work with slotted classes. - - - The `class.__subclasses__` attribute needs a garbage collection run (which can be manually triggered using `gc.collect`), for the original class to be removed. - See issue `#407 <https://github.com/python-attrs/attrs/issues/407>`_ for more details. - - -.. [#pypy] On PyPy, there is no memory advantage in using slotted classes. -.. [#pypyweakref] On PyPy, slotted classes are naturally weak-referenceable so ``weakref_slot=False`` has no effect. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/hashing.md b/tests/wpt/tests/tools/third_party/attrs/docs/hashing.md new file mode 100644 index 00000000000..231d818af6a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/hashing.md @@ -0,0 +1,85 @@ +# Hashing + +## Hash Method Generation + +:::{warning} +The overarching theme is to never set the `@attrs.define(unsafe_hash=X)` parameter yourself. +Leave it at `None` which means that *attrs* will do the right thing for you, depending on the other parameters: + +- If you want to make objects hashable by value: use `@define(frozen=True)`. +- If you want hashing and equality by object identity: use `@define(eq=False)` + +Setting `unsafe_hash` yourself can have unexpected consequences so we recommend to tinker with it only if you know exactly what you're doing. +::: + +Under certain circumstances, it's necessary for objects to be *hashable*. +For example if you want to put them into a {class}`set` or if you want to use them as keys in a {class}`dict`. + +The *hash* of an object is an integer that represents the contents of an object. +It can be obtained by calling {func}`hash` on an object and is implemented by writing a `__hash__` method for your class. + +*attrs* will happily write a `__hash__` method for you [^fn1], however it will *not* do so by default. +Because according to the [definition](https://docs.python.org/3/glossary.html#term-hashable) from the official Python docs, the returned hash has to fulfill certain constraints: + +[^fn1]: The hash is computed by hashing a tuple that consists of a unique id for the class plus all attribute values. + +1. Two objects that are equal, **must** have the same hash. + This means that if `x == y`, it *must* follow that `hash(x) == hash(y)`. + + By default, Python classes are compared *and* hashed by their `id`. + That means that every instance of a class has a different hash, no matter what attributes it carries. + + It follows that the moment you (or *attrs*) change the way equality is handled by implementing `__eq__` which is based on attribute values, this constraint is broken. + For that reason Python 3 will make a class that has customized equality unhashable. + Python 2 on the other hand will happily let you shoot your foot off. + Unfortunately, *attrs* still mimics (otherwise unsupported) Python 2's behavior for backward-compatibility reasons if you set `unsafe_hash=False`. + + The *correct way* to achieve hashing by id is to set `@define(eq=False)`. + Setting `@define(unsafe_hash=False)` (which implies `eq=True`) is almost certainly a *bug*. + + :::{warning} + Be careful when subclassing! + Setting `eq=False` on a class whose base class has a non-default `__hash__` method will *not* make *attrs* remove that `__hash__` for you. + + It is part of *attrs*'s philosophy to only *add* to classes so you have the freedom to customize your classes as you wish. + So if you want to *get rid* of methods, you'll have to do it by hand. + + The easiest way to reset `__hash__` on a class is adding `__hash__ = object.__hash__` in the class body. + ::: + +2. If two objects are not equal, their hash **should** be different. + + While this isn't a requirement from a standpoint of correctness, sets and dicts become less effective if there are a lot of identical hashes. + The worst case is when all objects have the same hash which turns a set into a list. + +3. The hash of an object **must not** change. + + If you create a class with `@define(frozen=True)` this is fulfilled by definition, therefore *attrs* will write a `__hash__` function for you automatically. + You can also force it to write one with `unsafe_hash=True` but then it's *your* responsibility to make sure that the object is not mutated. + + This point is the reason why mutable structures like lists, dictionaries, or sets aren't hashable while immutable ones like tuples or `frozenset`s are: + point 1 and 2 require that the hash changes with the contents but point 3 forbids it. + +For a more thorough explanation of this topic, please refer to this blog post: [*Python Hashes and Equality*](https://hynek.me/articles/hashes-and-equality/). + +:::{note} +Please note that the `unsafe_hash` argument's original name was `hash` but was changed to conform with {pep}`681` in 22.2.0. +The old argument name is still around and will **not** be removed -- but setting `unsafe_hash` takes precedence over `hash`. +The field-level argument is still called `hash` and will remain so. +::: + + +## Hashing and Mutability + +Changing any field involved in hash code computation after the first call to `__hash__` (typically this would be after its insertion into a hash-based collection) can result in silent bugs. +Therefore, it is strongly recommended that hashable classes be `frozen`. +Beware, however, that this is not a complete guarantee of safety: +if a field points to an object and that object is mutated, the hash code may change, but `frozen` will not protect you. + + +## Hash Code Caching + +Some objects have hash codes which are expensive to compute. +If such objects are to be stored in hash-based collections, it can be useful to compute the hash codes only once and then store the result on the object to make future hash code requests fast. +To enable caching of hash codes, pass `@define(cache_hash=True)`. +This may only be done if *attrs* is already generating a hash function for the object. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/hashing.rst b/tests/wpt/tests/tools/third_party/attrs/docs/hashing.rst deleted file mode 100644 index 30888f97bba..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/hashing.rst +++ /dev/null @@ -1,86 +0,0 @@ -Hashing -======= - -Hash Method Generation ----------------------- - -.. warning:: - - The overarching theme is to never set the ``@attr.s(hash=X)`` parameter yourself. - Leave it at ``None`` which means that ``attrs`` will do the right thing for you, depending on the other parameters: - - - If you want to make objects hashable by value: use ``@attr.s(frozen=True)``. - - If you want hashing and equality by object identity: use ``@attr.s(eq=False)`` - - Setting ``hash`` yourself can have unexpected consequences so we recommend to tinker with it only if you know exactly what you're doing. - -Under certain circumstances, it's necessary for objects to be *hashable*. -For example if you want to put them into a `set` or if you want to use them as keys in a `dict`. - -The *hash* of an object is an integer that represents the contents of an object. -It can be obtained by calling `hash` on an object and is implemented by writing a ``__hash__`` method for your class. - -``attrs`` will happily write a ``__hash__`` method for you [#fn1]_, however it will *not* do so by default. -Because according to the definition_ from the official Python docs, the returned hash has to fulfill certain constraints: - -#. Two objects that are equal, **must** have the same hash. - This means that if ``x == y``, it *must* follow that ``hash(x) == hash(y)``. - - By default, Python classes are compared *and* hashed by their `id`. - That means that every instance of a class has a different hash, no matter what attributes it carries. - - It follows that the moment you (or ``attrs``) change the way equality is handled by implementing ``__eq__`` which is based on attribute values, this constraint is broken. - For that reason Python 3 will make a class that has customized equality unhashable. - Python 2 on the other hand will happily let you shoot your foot off. - Unfortunately ``attrs`` currently mimics Python 2's behavior for backward compatibility reasons if you set ``hash=False``. - - The *correct way* to achieve hashing by id is to set ``@attr.s(eq=False)``. - Setting ``@attr.s(hash=False)`` (which implies ``eq=True``) is almost certainly a *bug*. - - .. warning:: - - Be careful when subclassing! - Setting ``eq=False`` on a class whose base class has a non-default ``__hash__`` method will *not* make ``attrs`` remove that ``__hash__`` for you. - - It is part of ``attrs``'s philosophy to only *add* to classes so you have the freedom to customize your classes as you wish. - So if you want to *get rid* of methods, you'll have to do it by hand. - - The easiest way to reset ``__hash__`` on a class is adding ``__hash__ = object.__hash__`` in the class body. - -#. If two object are not equal, their hash **should** be different. - - While this isn't a requirement from a standpoint of correctness, sets and dicts become less effective if there are a lot of identical hashes. - The worst case is when all objects have the same hash which turns a set into a list. - -#. The hash of an object **must not** change. - - If you create a class with ``@attr.s(frozen=True)`` this is fullfilled by definition, therefore ``attrs`` will write a ``__hash__`` function for you automatically. - You can also force it to write one with ``hash=True`` but then it's *your* responsibility to make sure that the object is not mutated. - - This point is the reason why mutable structures like lists, dictionaries, or sets aren't hashable while immutable ones like tuples or frozensets are: - point 1 and 2 require that the hash changes with the contents but point 3 forbids it. - -For a more thorough explanation of this topic, please refer to this blog post: `Python Hashes and Equality`_. - - -Hashing and Mutability ----------------------- - -Changing any field involved in hash code computation after the first call to ``__hash__`` (typically this would be after its insertion into a hash-based collection) can result in silent bugs. -Therefore, it is strongly recommended that hashable classes be ``frozen``. -Beware, however, that this is not a complete guarantee of safety: -if a field points to an object and that object is mutated, the hash code may change, but ``frozen`` will not protect you. - - -Hash Code Caching ------------------ - -Some objects have hash codes which are expensive to compute. -If such objects are to be stored in hash-based collections, it can be useful to compute the hash codes only once and then store the result on the object to make future hash code requests fast. -To enable caching of hash codes, pass ``cache_hash=True`` to ``@attrs``. -This may only be done if ``attrs`` is already generating a hash function for the object. - -.. [#fn1] The hash is computed by hashing a tuple that consists of an unique id for the class plus all attribute values. - -.. _definition: https://docs.python.org/3/glossary.html#term-hashable -.. _`Python Hashes and Equality`: https://hynek.me/articles/hashes-and-equality/ diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.md b/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.md new file mode 100644 index 00000000000..7acc8121322 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.md @@ -0,0 +1,118 @@ +(how)= + +# How Does It Work? + +## Boilerplate + +*attrs* isn't the first library that aims to simplify class definition in Python. +But its **declarative** approach combined with **no runtime overhead** lets it stand out. + +Once you apply the `@attrs.define` (or `@attr.s`) decorator to a class, *attrs* searches the class object for instances of `attr.ib`s. +Internally they're a representation of the data passed into `attr.ib` along with a counter to preserve the order of the attributes. +Alternatively, it's possible to define them using {doc}`types`. + +In order to ensure that subclassing works as you'd expect it to work, *attrs* also walks the class hierarchy and collects the attributes of all base classes. +Please note that *attrs* does *not* call `super()` *ever*. +It will write {term}`dunder methods` to work on *all* of those attributes which also has performance benefits due to fewer function calls. + +Once *attrs* knows what attributes it has to work on, it writes the requested {term}`dunder methods` and -- depending on whether you wish to have a {term}`dict <dict classes>` or {term}`slotted <slotted classes>` class -- creates a new class for you (`slots=True`) or attaches them to the original class (`slots=False`). +While creating new classes is more elegant, we've run into several edge cases surrounding metaclasses that make it impossible to go this route unconditionally. + +To be very clear: if you define a class with a single attribute without a default value, the generated `__init__` will look *exactly* how you'd expect: + +```{doctest} +>>> import inspect +>>> from attrs import define +>>> @define +... class C: +... x: int +>>> print(inspect.getsource(C.__init__)) +def __init__(self, x): + self.x = x +<BLANKLINE> +``` + +No magic, no meta programming, no expensive introspection at runtime. + +--- + +Everything until this point happens exactly *once* when the class is defined. +As soon as a class is done, it's done. +And it's just a regular Python class like any other, except for a single `__attrs_attrs__` attribute that *attrs* uses internally. +Much of the information is accessible via {func}`attrs.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of *attrs* (like {func}`attrs.asdict`). + +And once you start instantiating your classes, *attrs* is out of your way completely. + +This **static** approach was very much a design goal of *attrs* and what I strongly believe makes it distinct. + +(how-frozen)= + +## Immutability + +In order to give you immutability, *attrs* will attach a `__setattr__` method to your class that raises an {class}`attrs.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. + +The same is true if you choose to freeze individual attributes using the {obj}`attrs.setters.frozen` *on_setattr* hook -- except that the exception becomes {class}`attrs.exceptions.FrozenAttributeError`. + +Both exceptions subclass {class}`attrs.exceptions.FrozenError`. + +--- + +Depending on whether a class is a dict class or a slotted class, *attrs* uses a different technique to circumvent that limitation in the `__init__` method. + +Once constructed, frozen instances don't differ in any way from regular ones except that you cannot change its attributes. + +### Dict Classes + +Dict classes -- i.e. regular classes -- simply assign the value directly into the class' eponymous `__dict__` (and there's nothing we can do to stop the user to do the same). + +The performance impact is negligible. + +### Slotted Classes + +Slotted classes are more complicated. +Here it uses (an aggressively cached) {meth}`object.__setattr__` to set your attributes. +This is (still) slower than a plain assignment: + +```none +$ pyperf timeit --rigorous \ + -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \ + "C(1, 2, 3)" +......................................... +Mean +- std dev: 228 ns +- 18 ns + +$ pyperf timeit --rigorous \ + -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \ + "C(1, 2, 3)" +......................................... +Mean +- std dev: 425 ns +- 16 ns +``` + +So on a laptop computer the difference is about 200 nanoseconds (1 second is 1,000,000,000 nanoseconds). +It's certainly something you'll feel in a hot loop but shouldn't matter in normal code. +Pick what's more important to you. + +### Summary + +You should avoid instantiating lots of frozen slotted classes (i.e. `@frozen`) in performance-critical code. + +Frozen dict classes have barely a performance impact, unfrozen slotted classes are even *faster* than unfrozen dict classes (i.e. regular classes). + + +(how-slotted-cached_property)= + +## Cached Properties on Slotted Classes. + +By default, the standard library `functools.cached_property` decorator does not work on slotted classes, +because it requires a `__dict__` to store the cached value. +This could be surprising when uses *attrs*, as makes using slotted classes so easy, +so attrs will convert `functools.cached_property` decorated methods, when constructing slotted classes. + +Getting this working is achieved by: +* Adding names to `__slots__` for the wrapped methods. +* Adding a `__getattr__` method to set values on the wrapped methods. + +For most users this should mean that it works transparently. + +Note that the implementation does not guarantee that the wrapped method is called +only once in multi-threaded usage. This matches the implementation of `cached_property` +in python v3.12. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.rst b/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.rst deleted file mode 100644 index f899740542c..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/how-does-it-work.rst +++ /dev/null @@ -1,109 +0,0 @@ -.. _how: - -How Does It Work? -================= - - -Boilerplate ------------ - -``attrs`` certainly isn't the first library that aims to simplify class definition in Python. -But its **declarative** approach combined with **no runtime overhead** lets it stand out. - -Once you apply the ``@attrs.define`` (or ``@attr.s``) decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. -Internally they're a representation of the data passed into ``attr.ib`` along with a counter to preserve the order of the attributes. -Alternatively, it's possible to define them using :doc:`types`. - -In order to ensure that subclassing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all base classes. -Please note that ``attrs`` does *not* call ``super()`` *ever*. -It will write :term:`dunder methods` to work on *all* of those attributes which also has performance benefits due to fewer function calls. - -Once ``attrs`` knows what attributes it has to work on, it writes the requested :term:`dunder methods` and -- depending on whether you wish to have a :term:`dict <dict classes>` or :term:`slotted <slotted classes>` class -- creates a new class for you (``slots=True``) or attaches them to the original class (``slots=False``). -While creating new classes is more elegant, we've run into several edge cases surrounding metaclasses that make it impossible to go this route unconditionally. - -To be very clear: if you define a class with a single attribute without a default value, the generated ``__init__`` will look *exactly* how you'd expect: - -.. doctest:: - - >>> import inspect - >>> from attr import define - >>> @define - ... class C: - ... x: int - >>> print(inspect.getsource(C.__init__)) - def __init__(self, x): - self.x = x - <BLANKLINE> - -No magic, no meta programming, no expensive introspection at runtime. - -**** - -Everything until this point happens exactly *once* when the class is defined. -As soon as a class is done, it's done. -And it's just a regular Python class like any other, except for a single ``__attrs_attrs__`` attribute that ``attrs`` uses internally. -Much of the information is accessible via `attrs.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like `attrs.asdict`). - -And once you start instantiating your classes, ``attrs`` is out of your way completely. - -This **static** approach was very much a design goal of ``attrs`` and what I strongly believe makes it distinct. - - -.. _how-frozen: - -Immutability ------------- - -In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attrs.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. - -The same is true if you choose to freeze individual attributes using the `attrs.setters.frozen` *on_setattr* hook -- except that the exception becomes `attrs.exceptions.FrozenAttributeError`. - -Both errors subclass `attrs.exceptions.FrozenError`. - ------ - -Depending on whether a class is a dict class or a slotted class, ``attrs`` uses a different technique to circumvent that limitation in the ``__init__`` method. - -Once constructed, frozen instances don't differ in any way from regular ones except that you cannot change its attributes. - - -Dict Classes -++++++++++++ - -Dict classes -- i.e. regular classes -- simply assign the value directly into the class' eponymous ``__dict__`` (and there's nothing we can do to stop the user to do the same). - -The performance impact is negligible. - - -Slotted Classes -+++++++++++++++ - -Slotted classes are more complicated. -Here it uses (an aggressively cached) :meth:`object.__setattr__` to set your attributes. -This is (still) slower than a plain assignment: - -.. code-block:: none - - $ pyperf timeit --rigorous \ - -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True)" \ - "C(1, 2, 3)" - ........................................ - Median +- std dev: 378 ns +- 12 ns - - $ pyperf timeit --rigorous \ - -s "import attr; C = attr.make_class('C', ['x', 'y', 'z'], slots=True, frozen=True)" \ - "C(1, 2, 3)" - ........................................ - Median +- std dev: 676 ns +- 16 ns - -So on a laptop computer the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds). -It's certainly something you'll feel in a hot loop but shouldn't matter in normal code. -Pick what's more important to you. - - -Summary -+++++++ - -You should avoid instantiating lots of frozen slotted classes (i.e. ``@frozen``) in performance-critical code. - -Frozen dict classes have barely a performance impact, unfrozen slotted classes are even *faster* than unfrozen dict classes (i.e. regular classes). diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/index.md b/tests/wpt/tests/tools/third_party/attrs/docs/index.md new file mode 100644 index 00000000000..ad92b5a3982 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/index.md @@ -0,0 +1,89 @@ +# *attrs*: Classes Without Boilerplate + +Release **{sub-ref}`release`** ([What's new?](changelog.md)) + +```{include} ../README.md +:start-after: 'teaser-begin -->' +:end-before: '<!-- teaser-end' +``` + + +## Getting Started + +*attrs* is a Python-only package [hosted on PyPI](https://pypi.org/project/attrs/). +The recommended installation method is [pip](https://pip.pypa.io/en/stable/)-installing into a [virtualenv](https://hynek.me/articles/virtualenv-lives/): + +```console +$ python -Im pip install attrs +``` + +The next steps will get you up and running in no time: + +- {doc}`overview` will show you a simple example of *attrs* in action and introduce you to its philosophy. + Afterwards, you can start writing your own classes and understand what drives *attrs*'s design. +- {doc}`examples` will give you a comprehensive tour of *attrs*'s features. + After reading, you will know about our advanced features and how to use them. +- {doc}`why` gives you a rundown of potential alternatives and why we think *attrs* is still worthwhile -- depending on *your* needs even superior. +- If at any point you get confused by some terminology, please check out our {doc}`glossary`. + +If you need any help while getting started, feel free to use the `python-attrs` tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs) and someone will surely help you out! + + +## Day-to-Day Usage + +- {doc}`types` help you to write *correct* and *self-documenting* code. + *attrs* has first class support for them, yet keeps them optional if you’re not convinced! +- Instance initialization is one of *attrs* key feature areas. + Our goal is to relieve you from writing as much code as possible. + {doc}`init` gives you an overview what *attrs* has to offer and explains some related philosophies we believe in. +- Comparing and ordering objects is a common task. + {doc}`comparison` shows you how *attrs* helps you with that and how you can customize it. +- If you want to put objects into sets or use them as keys in dictionaries, they have to be hashable. + The simplest way to do that is to use frozen classes, but the topic is more complex than it seems and {doc}`hashing` will give you a primer on what to look out for. +- Once you're comfortable with the concepts, our {doc}`api` contains all information you need to use *attrs* to its fullest. +- *attrs* is built for extension from the ground up. + {doc}`extending` will show you the affordances it offers and how to make it a building block of your own projects. +- Finally, if you're confused by all the `attr.s`, `attr.ib`, `attrs`, `attrib`, `define`, `frozen`, and `field`, head over to {doc}`names` for a very short explanation, and optionally a quick history lesson. + + +## *attrs* for Enterprise + +```{include} ../README.md +:start-after: '### *attrs* for Enterprise' +``` + +--- + +## Full Table of Contents + +```{toctree} +:maxdepth: 2 + +overview +why +examples +types +init +comparison +hashing +api +api-attr +extending +how-does-it-work +glossary +``` + +```{toctree} +:caption: Meta +:maxdepth: 1 + +names +license +changelog +Third-party Extentions <https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs> +PyPI <https://pypi.org/project/attrs/> +Contributing <https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md> +Funding <https://hynek.me/say-thanks/> +``` + +[Full Index](genindex) diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/index.rst b/tests/wpt/tests/tools/third_party/attrs/docs/index.rst deleted file mode 100644 index ff65a6738c9..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/index.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. module:: attr -.. module:: attrs - -====================================== -``attrs``: Classes Without Boilerplate -====================================== - -Release v\ |release| (`What's new? <changelog>`). - -.. include:: ../README.rst - :start-after: teaser-begin - :end-before: teaser-end - - -Getting Started -=============== - -``attrs`` is a Python-only package `hosted on PyPI <https://pypi.org/project/attrs/>`_. -The recommended installation method is `pip <https://pip.pypa.io/en/stable/>`_-installing into a `virtualenv <https://hynek.me/articles/virtualenv-lives/>`_: - -.. code-block:: console - - $ python -m pip install attrs - -The next three steps should bring you up and running in no time: - -- `overview` will show you a simple example of ``attrs`` in action and introduce you to its philosophy. - Afterwards, you can start writing your own classes and understand what drives ``attrs``'s design. -- `examples` will give you a comprehensive tour of ``attrs``'s features. - After reading, you will know about our advanced features and how to use them. -- If you're confused by all the ``attr.s``, ``attr.ib``, ``attrs``, ``attrib``, ``define``, ``frozen``, and ``field``, head over to `names` for a very short explanation, and optionally a quick history lesson. -- Finally `why` gives you a rundown of potential alternatives and why we think ``attrs`` is superior. - Yes, we've heard about ``namedtuple``\ s and Data Classes! -- If at any point you get confused by some terminology, please check out our `glossary`. - - -If you need any help while getting started, feel free to use the ``python-attrs`` tag on `Stack Overflow <https://stackoverflow.com/questions/tagged/python-attrs>`_ and someone will surely help you out! - - -Day-to-Day Usage -================ - -- `types` help you to write *correct* and *self-documenting* code. - ``attrs`` has first class support for them, yet keeps them optional if you’re not convinced! -- Instance initialization is one of ``attrs`` key feature areas. - Our goal is to relieve you from writing as much code as possible. - `init` gives you an overview what ``attrs`` has to offer and explains some related philosophies we believe in. -- Comparing and ordering objects is a common task. - `comparison` shows you how ``attrs`` helps you with that and how you can customize it. -- If you want to put objects into sets or use them as keys in dictionaries, they have to be hashable. - The simplest way to do that is to use frozen classes, but the topic is more complex than it seems and `hashing` will give you a primer on what to look out for. -- Once you're comfortable with the concepts, our `api` contains all information you need to use ``attrs`` to its fullest. -- ``attrs`` is built for extension from the ground up. - `extending` will show you the affordances it offers and how to make it a building block of your own projects. - - -.. include:: ../README.rst - :start-after: -getting-help- - :end-before: -project-information- - - ----- - - -Full Table of Contents -====================== - -.. toctree:: - :maxdepth: 2 - - overview - why - examples - types - init - comparison - hashing - api - extending - how-does-it-work - names - glossary - - -.. include:: ../README.rst - :start-after: -project-information- - -.. toctree:: - :maxdepth: 1 - - license - python-2 - changelog - - -Indices and tables -================== - -* `genindex` -* `search` diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/init.md b/tests/wpt/tests/tools/third_party/attrs/docs/init.md new file mode 100644 index 00000000000..4aaa099957f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/init.md @@ -0,0 +1,514 @@ +# Initialization + +In Python, instance initialization happens in the `__init__` method. +Generally speaking, you should keep as little logic as possible in it, and you should think about what the class needs and not how it is going to be instantiated. + +Passing complex objects into `__init__` and then using them to derive data for the class unnecessarily couples your new class with the old class which makes it harder to test and also will cause problems later. + +So assuming you use an ORM and want to extract 2D points from a row object, do not write code like this: + +```python +class Point: + def __init__(self, database_row): + self.x = database_row.x + self.y = database_row.y + +pt = Point(row) +``` + +Instead, write a {obj}`classmethod` that will extract it for you: + +```python +@define +class Point: + x: float + y: float + + @classmethod + def from_row(cls, row): + return cls(row.x, row.y) + +pt = Point.from_row(row) +``` + +Now you can instantiate `Point`s without creating fake row objects in your tests and you can have as many smart creation helpers as you want, in case more data sources appear. + +For similar reasons, we strongly discourage from patterns like: + +```python +pt = Point(**row.attributes) +``` + +which couples your classes to the database data model. +Try to design your classes in a way that is clean and convenient to use -- not based on your database format. +The database format can change anytime and you're stuck with a bad class design that is hard to change. +Embrace functions and classmethods as a filter between reality and what's best for you to work with. + +:::{warning} +While *attrs*'s initialization concepts (including the following sections about validators and converters) are powerful, they are **not** intended to replace a fully-featured serialization or validation system. + +We want to help you to write a `__init__` that you'd write by hand, but with less boilerplate. + +If you look for powerful-yet-unintrusive serialization and validation for your *attrs* classes, have a look at our sibling project [*cattrs*](https://catt.rs/) or our [third-party extensions](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs). +::: + +(private-attributes)= + +## Private Attributes and Aliases + +One thing people tend to find confusing is the treatment of private attributes that start with an underscore. +*attrs* follows the doctrine that [there is no such thing as a private argument](https://github.com/hynek/characteristic/issues/6) and strips the underscores from the name when writing the `__init__` method signature: + +```{doctest} +>>> import inspect +>>> from attrs import define +>>> @define +... class C: +... _x: int +>>> inspect.signature(C.__init__) +<Signature (self, x: int) -> None> +``` + +There really isn't a right or wrong, it's a matter of taste. +But it's important to be aware of it because it can lead to surprising syntax errors: + +```{doctest} +>>> @define +... class C: +... _1: int +Traceback (most recent call last): + ... +SyntaxError: invalid syntax +``` + +In this case a valid attribute name `_1` got transformed into an invalid argument name `1`. + +If your taste differs, you can use the *alias* argument to {func}`attrs.field` to explicitly set the argument name. +This can be used to override private attribute handling, or make other arbitrary changes to `__init__` argument names. + +```{doctest} +>>> from attrs import field +>>> @define +... class C: +... _x: int = field(alias="_x") +... y: int = field(alias="distasteful_y") +>>> inspect.signature(C.__init__) +<Signature (self, _x: int, distasteful_y: int) -> None> +``` + +(defaults)= + +## Defaults + +Sometimes you don't want to pass all attribute values to a class. +And sometimes, certain attributes aren't even intended to be passed but you want to allow for customization anyways for easier testing. + +This is when default values come into play: + +```{doctest} +>>> from attrs import Factory +>>> @define +... class C: +... a: int = 42 +... b: list = field(factory=list) +... c: list = Factory(list) # syntactic sugar for above +... d: dict = field() +... @d.default +... def _any_name_except_a_name_of_an_attribute(self): +... return {} +>>> C() +C(a=42, b=[], c=[], d={}) +``` + +It's important that the decorated method -- or any other method or property! -- doesn't have the same name as the attribute, otherwise it would overwrite the attribute definition. + +Please note that as with function and method signatures, `default=[]` will *not* do what you may think it might do: + +```{doctest} +>>> @define +... class C: +... x = [] +>>> i = C() +>>> k = C() +>>> i.x.append(42) +>>> k.x +[42] +``` + +This is why *attrs* comes with factory options. + +:::{warning} +Please note that the decorator based defaults have one gotcha: +they are executed when the attribute is set, that means depending on the order of attributes, the `self` object may not be fully initialized when they're called. + +Therefore you should use `self` as little as possible. + +Even the smartest of us can [get confused](https://github.com/python-attrs/attrs/issues/289) by what happens if you pass partially initialized objects around. +::: + +(validators)= + +## Validators + +Another thing that definitely *does* belong in `__init__` is checking the resulting instance for invariants. +This is why *attrs* has the concept of validators. + + +### Decorator + +The most straightforward way is using the attribute's `validator` method as a decorator. + +The method has to accept three arguments: + +1. the *instance* that's being validated (aka `self`), +2. the *attribute* that it's validating, and finally +3. the *value* that is passed for it. + +These values are passed as *positional arguments*, therefore their names don't matter. + +If the value does not pass the validator's standards, it just raises an appropriate exception. + +```{doctest} +>>> @define +... class C: +... x: int = field() +... @x.validator +... def _check_x(self, attribute, value): +... if value > 42: +... raise ValueError("x must be smaller or equal to 42") +>>> C(42) +C(x=42) +>>> C(43) +Traceback (most recent call last): + ... +ValueError: x must be smaller or equal to 42 +``` + +Again, it's important that the decorated method doesn't have the same name as the attribute and that the {func}`attrs.field` helper is used. + + +### Callables + +If you want to re-use your validators, you should have a look at the `validator` argument to {func}`attrs.field`. + +It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach. +Also as with the decorator approach, they are passed as *positional arguments* so you can name them however you want. + +Since the validators run *after* the instance is initialized, you can refer to other attributes while validating: + +```{doctest} +>>> import attrs +>>> def x_smaller_than_y(instance, attribute, value): +... if value >= instance.y: +... raise ValueError("'x' has to be smaller than 'y'!") +>>> @define +... class C: +... x = field(validator=[attrs.validators.instance_of(int), +... x_smaller_than_y]) +... y = field() +>>> C(x=3, y=4) +C(x=3, y=4) +>>> C(x=4, y=3) +Traceback (most recent call last): + ... +ValueError: 'x' has to be smaller than 'y'! +``` + +This example demonstrates a convenience shortcut: +Passing a list of validators directly is equivalent to passing them wrapped in the {obj}`attrs.validators.and_` validator and all validators must pass. + +*attrs* won't intercept your changes to those attributes but you can always call {func}`attrs.validate` on any instance to verify that it's still valid: + +When using {func}`attrs.define` or [`attrs.frozen`](attrs.frozen), however, *attrs* will run the validators even when setting the attribute. + +```{doctest} +>>> i = C(4, 5) +>>> i.x = 5 +Traceback (most recent call last): + ... +ValueError: 'x' has to be smaller than 'y'! +``` + +*attrs* ships with a bunch of validators, make sure to [check them out]( api-validators) before writing your own: + +```{doctest} +>>> @define +... class C: +... x = field(validator=attrs.validators.instance_of(int)) +>>> C(42) +C(x=42) +>>> C("42") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42') +``` + +Of course you can mix and match the two approaches at your convenience. +If you use both ways to define validators for an attribute, they are both ran: + +```{doctest} +>>> @define +... class C: +... x = field(validator=attrs.validators.instance_of(int)) +... @x.validator +... def fits_byte(self, attribute, value): +... if not 0 <= value < 256: +... raise ValueError("value out of bounds") +>>> C(128) +C(x=128) +>>> C("128") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=one), <class 'int'>, '128') +>>> C(256) +Traceback (most recent call last): + ... +ValueError: value out of bounds +``` + +Finally, validators can be globally disabled: + +```{doctest} +>>> attrs.validators.set_disabled(True) +>>> C("128") +C(x='128') +>>> attrs.validators.set_disabled(False) +>>> C("128") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128') +``` + +... or within a context manager: + +```{doctest} +>>> with attrs.validators.disabled(): +... C("128") +C(x='128') +>>> C("128") +Traceback (most recent call last): + ... +TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128') +``` + +(converters)= + +## Converters + +Sometimes, it is necessary to normalize the values coming in, therefore *attrs* comes with converters. + +Attributes can have a `converter` function specified, which will be called with the attribute's passed-in value to get a new value to use. +This can be useful for doing type-conversions on values that you don't want to force your callers to do. + +```{doctest} +>>> @define +... class C: +... x = field(converter=int) +>>> o = C("1") +>>> o.x +1 +>>> o.x = "2" +>>> o.x +2 +``` + +Converters are run *before* validators, so you can use validators to check the final form of the value. + +```{doctest} +>>> def validate_x(instance, attribute, value): +... if value < 0: +... raise ValueError("x must be at least 0.") +>>> @define +... class C: +... x = field(converter=int, validator=validate_x) +>>> o = C("0") +>>> o.x +0 +>>> C("-1") +Traceback (most recent call last): + ... +ValueError: x must be at least 0. +``` + +Arguably, you can abuse converters as one-argument validators: + +```{doctest} +>>> C("x") +Traceback (most recent call last): + ... +ValueError: invalid literal for int() with base 10: 'x' +``` + +If a converter's first argument has a type annotation, that type will appear in the signature for `__init__`. +A converter will override an explicit type annotation or `type` argument. + +```{doctest} +>>> def str2int(x: str) -> int: +... return int(x) +>>> @define +... class C: +... x = field(converter=str2int) +>>> C.__init__.__annotations__ +{'return': None, 'x': <class 'str'>} +``` + + +## Hooking Yourself Into Initialization + +Generally speaking, the moment you realize the need of finer control – than what *attrs* offers – over how a class is instantiated, it's usually best to use a {obj}`classmethod` factory or to apply the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern). + +However, sometimes you need to do that one quick thing before or after your class is initialized. +For that purpose, *attrs* offers the following options: + +- `__attrs_pre_init__` is automatically detected and run *before* *attrs* starts initializing. + If `__attrs_pre_init__` takes more than the `self` argument, the *attrs*-generated `__init__` will call it with the same arguments it received itself. + This is useful if you need to inject a call to `super().__init__()` -- with or without arguments. + +- `__attrs_post_init__` is automatically detected and run *after* *attrs* is done initializing your instance. + This is useful if you want to derive some attribute from others or perform some kind of validation over the whole instance. + +- `__attrs_init__` is written and attached to your class *instead* of `__init__`, if *attrs* is told to not write one (i.e. `init=False` or a combination of `auto_detect=True` and a custom `__init__`). + This is useful if you want full control over the initialization process, but don't want to set the attributes by hand. + + +### Pre Init + +The sole reason for the existence of `__attrs_pre_init__` is to give users the chance to call `super().__init__()`, because some subclassing-based APIs require that. + +```{doctest} +>>> @define +... class C: +... x: int +... def __attrs_pre_init__(self): +... super().__init__() +>>> C(42) +C(x=42) +``` + +If you need more control, use the custom init approach described next. + + +### Custom Init + +If you tell *attrs* to not write an `__init__`, it will write an `__attrs_init__` instead, with the same code that it would have used for `__init__`. +You have full control over the initialization, but also have to type out the types of your arguments etc. +Here's an example of a manual default value: + +```{doctest} +>>> @define +... class C: +... x: int +... +... def __init__(self, x: int = 42): +... self.__attrs_init__(x) +>>> C() +C(x=42) +``` + + +### Post Init + +```{doctest} +>>> @define +... class C: +... x: int +... y: int = field(init=False) +... def __attrs_post_init__(self): +... self.y = self.x + 1 +>>> C(1) +C(x=1, y=2) +``` + +Please note that you can't directly set attributes on frozen classes: + +```{doctest} +>>> @frozen +... class FrozenBroken: +... x: int +... y: int = field(init=False) +... def __attrs_post_init__(self): +... self.y = self.x + 1 +>>> FrozenBroken(1) +Traceback (most recent call last): + ... +attrs.exceptions.FrozenInstanceError: can't set attribute +``` + +If you need to set attributes on a frozen class, you'll have to resort to the [same trick](how-frozen) as *attrs* and use {meth}`object.__setattr__`: + +```{doctest} +>>> @define +... class Frozen: +... x: int +... y: int = field(init=False) +... def __attrs_post_init__(self): +... object.__setattr__(self, "y", self.x + 1) +>>> Frozen(1) +Frozen(x=1, y=2) +``` + +Note that you *must not* access the hash code of the object in `__attrs_post_init__` if `cache_hash=True`. + + +## Order of Execution + +If present, the hooks are executed in the following order: + +1. `__attrs_pre_init__` (if present on *current* class) + +2. For each attribute, in the order it was declared: + + 1. default factory + 2. converter + +3. *all* validators + +4. `__attrs_post_init__` (if present on *current* class) + +Notably this means, that you can access all attributes from within your validators, but your converters have to deal with invalid values and have to return a valid value. + + +## Derived Attributes + +One of the most common *attrs* questions on *Stack Overflow* is how to have attributes that depend on other attributes. +For example if you have an API token and want to instantiate a web client that uses it for authentication. +Based on the previous sections, there are two approaches. + +The simpler one is using `__attrs_post_init__`: + +```python +@define +class APIClient: + token: str + client: WebClient = field(init=False) + + def __attrs_post_init__(self): + self.client = WebClient(self.token) +``` + +The second one is using a decorator-based default: + +```python +@define +class APIClient: + token: str + client: WebClient = field() # needed! attr.ib works too + + @client.default + def _client_factory(self): + return WebClient(self.token) +``` + +That said, and as pointed out in the beginning of the chapter, a better approach would be to have a factory class method: + +```python +@define +class APIClient: + client: WebClient + + @classmethod + def from_token(cls, token: str) -> "APIClient": + return cls(client=WebClient(token)) +``` + +This makes the class more testable. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/init.rst b/tests/wpt/tests/tools/third_party/attrs/docs/init.rst deleted file mode 100644 index fb276ded8a2..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/init.rst +++ /dev/null @@ -1,489 +0,0 @@ -Initialization -============== - -In Python, instance initialization happens in the ``__init__`` method. -Generally speaking, you should keep as little logic as possible in it, and you should think about what the class needs and not how it is going to be instantiated. - -Passing complex objects into ``__init__`` and then using them to derive data for the class unnecessarily couples your new class with the old class which makes it harder to test and also will cause problems later. - -So assuming you use an ORM and want to extract 2D points from a row object, do not write code like this:: - - class Point(object): - def __init__(self, database_row): - self.x = database_row.x - self.y = database_row.y - - pt = Point(row) - -Instead, write a `classmethod` that will extract it for you:: - - @define - class Point: - x: float - y: float - - @classmethod - def from_row(cls, row): - return cls(row.x, row.y) - - pt = Point.from_row(row) - -Now you can instantiate ``Point``\ s without creating fake row objects in your tests and you can have as many smart creation helpers as you want, in case more data sources appear. - -For similar reasons, we strongly discourage from patterns like:: - - pt = Point(**row.attributes) - -which couples your classes to the database data model. -Try to design your classes in a way that is clean and convenient to use -- not based on your database format. -The database format can change anytime and you're stuck with a bad class design that is hard to change. -Embrace functions and classmethods as a filter between reality and what's best for you to work with. - -If you look for object serialization, there's a bunch of projects listed on our ``attrs`` extensions `Wiki page`_. -Some of them even support nested schemas. - - -Private Attributes ------------------- - -One thing people tend to find confusing is the treatment of private attributes that start with an underscore. -``attrs`` follows the doctrine that `there is no such thing as a private argument`_ and strips the underscores from the name when writing the ``__init__`` method signature: - -.. doctest:: - - >>> import inspect, attr, attrs - >>> from attr import define - >>> @define - ... class C: - ... _x: int - >>> inspect.signature(C.__init__) - <Signature (self, x: int) -> None> - -There really isn't a right or wrong, it's a matter of taste. -But it's important to be aware of it because it can lead to surprising syntax errors: - -.. doctest:: - - >>> @define - ... class C: - ... _1: int - Traceback (most recent call last): - ... - SyntaxError: invalid syntax - -In this case a valid attribute name ``_1`` got transformed into an invalid argument name ``1``. - - -Defaults --------- - -Sometimes you don't want to pass all attribute values to a class. -And sometimes, certain attributes aren't even intended to be passed but you want to allow for customization anyways for easier testing. - -This is when default values come into play: - -.. doctest:: - - >>> from attr import define, field, Factory - - >>> @define - ... class C: - ... a: int = 42 - ... b: list = field(factory=list) - ... c: list = Factory(list) # syntactic sugar for above - ... d: dict = field() - ... @d.default - ... def _any_name_except_a_name_of_an_attribute(self): - ... return {} - >>> C() - C(a=42, b=[], c=[], d={}) - -It's important that the decorated method -- or any other method or property! -- doesn't have the same name as the attribute, otherwise it would overwrite the attribute definition. - -Please note that as with function and method signatures, ``default=[]`` will *not* do what you may think it might do: - -.. doctest:: - - >>> @define - ... class C: - ... x = [] - >>> i = C() - >>> k = C() - >>> i.x.append(42) - >>> k.x - [42] - - -This is why ``attrs`` comes with factory options. - -.. warning:: - - Please note that the decorator based defaults have one gotcha: - they are executed when the attribute is set, that means depending on the order of attributes, the ``self`` object may not be fully initialized when they're called. - - Therefore you should use ``self`` as little as possible. - - Even the smartest of us can `get confused`_ by what happens if you pass partially initialized objects around. - - -.. _validators: - -Validators ----------- - -Another thing that definitely *does* belong in ``__init__`` is checking the resulting instance for invariants. -This is why ``attrs`` has the concept of validators. - - -Decorator -~~~~~~~~~ - -The most straightforward way is using the attribute's ``validator`` method as a decorator. - -The method has to accept three arguments: - -#. the *instance* that's being validated (aka ``self``), -#. the *attribute* that it's validating, and finally -#. the *value* that is passed for it. - -If the value does not pass the validator's standards, it just raises an appropriate exception. - - >>> @define - ... class C: - ... x: int = field() - ... @x.validator - ... def _check_x(self, attribute, value): - ... if value > 42: - ... raise ValueError("x must be smaller or equal to 42") - >>> C(42) - C(x=42) - >>> C(43) - Traceback (most recent call last): - ... - ValueError: x must be smaller or equal to 42 - -Again, it's important that the decorated method doesn't have the same name as the attribute and that the `attrs.field()` helper is used. - - -Callables -~~~~~~~~~ - -If you want to re-use your validators, you should have a look at the ``validator`` argument to `attrs.field`. - -It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach. - -Since the validators run *after* the instance is initialized, you can refer to other attributes while validating: - -.. doctest:: - - >>> def x_smaller_than_y(instance, attribute, value): - ... if value >= instance.y: - ... raise ValueError("'x' has to be smaller than 'y'!") - >>> @define - ... class C: - ... x = field(validator=[attrs.validators.instance_of(int), - ... x_smaller_than_y]) - ... y = field() - >>> C(x=3, y=4) - C(x=3, y=4) - >>> C(x=4, y=3) - Traceback (most recent call last): - ... - ValueError: 'x' has to be smaller than 'y'! - -This example also shows of some syntactic sugar for using the `attrs.validators.and_` validator: if you pass a list, all validators have to pass. - -``attrs`` won't intercept your changes to those attributes but you can always call `attrs.validate` on any instance to verify that it's still valid: -When using `attrs.define` or `attrs.frozen`, ``attrs`` will run the validators even when setting the attribute. - -.. doctest:: - - >>> i = C(4, 5) - >>> i.x = 5 - Traceback (most recent call last): - ... - ValueError: 'x' has to be smaller than 'y'! - -``attrs`` ships with a bunch of validators, make sure to `check them out <api_validators>` before writing your own: - -.. doctest:: - - >>> @define - ... class C: - ... x = field(validator=attrs.validators.instance_of(int)) - >>> C(42) - C(x=42) - >>> C("42") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42') - -Of course you can mix and match the two approaches at your convenience. -If you define validators both ways for an attribute, they are both ran: - -.. doctest:: - - >>> @define - ... class C: - ... x = field(validator=attrs.validators.instance_of(int)) - ... @x.validator - ... def fits_byte(self, attribute, value): - ... if not 0 <= value < 256: - ... raise ValueError("value out of bounds") - >>> C(128) - C(x=128) - >>> C("128") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=one), <class 'int'>, '128') - >>> C(256) - Traceback (most recent call last): - ... - ValueError: value out of bounds - -And finally you can disable validators globally: - - >>> attrs.validators.set_disabled(True) - >>> C("128") - C(x='128') - >>> attrs.validators.set_disabled(False) - >>> C("128") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128') - -You can achieve the same by using the context manager: - - >>> with attrs.validators.disabled(): - ... C("128") - C(x='128') - >>> C("128") - Traceback (most recent call last): - ... - TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128') - - -.. _converters: - -Converters ----------- - -Finally, sometimes you may want to normalize the values coming in. -For that ``attrs`` comes with converters. - -Attributes can have a ``converter`` function specified, which will be called with the attribute's passed-in value to get a new value to use. -This can be useful for doing type-conversions on values that you don't want to force your callers to do. - -.. doctest:: - - >>> @define - ... class C: - ... x = field(converter=int) - >>> o = C("1") - >>> o.x - 1 - -Converters are run *before* validators, so you can use validators to check the final form of the value. - -.. doctest:: - - >>> def validate_x(instance, attribute, value): - ... if value < 0: - ... raise ValueError("x must be at least 0.") - >>> @define - ... class C: - ... x = field(converter=int, validator=validate_x) - >>> o = C("0") - >>> o.x - 0 - >>> C("-1") - Traceback (most recent call last): - ... - ValueError: x must be at least 0. - - -Arguably, you can abuse converters as one-argument validators: - -.. doctest:: - - >>> C("x") - Traceback (most recent call last): - ... - ValueError: invalid literal for int() with base 10: 'x' - - -If a converter's first argument has a type annotation, that type will appear in the signature for ``__init__``. -A converter will override an explicit type annotation or ``type`` argument. - -.. doctest:: - - >>> def str2int(x: str) -> int: - ... return int(x) - >>> @define - ... class C: - ... x = field(converter=str2int) - >>> C.__init__.__annotations__ - {'return': None, 'x': <class 'str'>} - - -Hooking Yourself Into Initialization ------------------------------------- - -Generally speaking, the moment you think that you need finer control over how your class is instantiated than what ``attrs`` offers, it's usually best to use a classmethod factory or to apply the `builder pattern <https://en.wikipedia.org/wiki/Builder_pattern>`_. - -However, sometimes you need to do that one quick thing before or after your class is initialized. -And for that ``attrs`` offers three means: - -- ``__attrs_pre_init__`` is automatically detected and run *before* ``attrs`` starts initializing. - This is useful if you need to inject a call to ``super().__init__()``. -- ``__attrs_post_init__`` is automatically detected and run *after* ``attrs`` is done initializing your instance. - This is useful if you want to derive some attribute from others or perform some kind of validation over the whole instance. -- ``__attrs_init__`` is written and attached to your class *instead* of ``__init__``, if ``attrs`` is told to not write one (i.e. ``init=False`` or a combination of ``auto_detect=True`` and a custom ``__init__``). - This is useful if you want full control over the initialization process, but don't want to set the attributes by hand. - - -Pre Init -~~~~~~~~ - -The sole reason for the existance of ``__attrs_pre_init__`` is to give users the chance to call ``super().__init__()``, because some subclassing-based APIs require that. - -.. doctest:: - - >>> @define - ... class C: - ... x: int - ... def __attrs_pre_init__(self): - ... super().__init__() - >>> C(42) - C(x=42) - -If you need more control, use the custom init approach described next. - - -Custom Init -~~~~~~~~~~~ - -If you tell ``attrs`` to not write an ``__init__``, it will write an ``__attrs_init__`` instead, with the same code that it would have used for ``__init__``. -You have full control over the initialization, but also have to type out the types of your arguments etc. -Here's an example of a manual default value: - -.. doctest:: - - >>> from typing import Optional - - >>> @define - ... class C: - ... x: int - ... - ... def __init__(self, x: int = 42): - ... self.__attrs_init__(x) - >>> C() - C(x=42) - - -Post Init -~~~~~~~~~ - -.. doctest:: - - >>> @define - ... class C: - ... x: int - ... y: int = field(init=False) - ... def __attrs_post_init__(self): - ... self.y = self.x + 1 - >>> C(1) - C(x=1, y=2) - -Please note that you can't directly set attributes on frozen classes: - -.. doctest:: - - >>> @frozen - ... class FrozenBroken: - ... x: int - ... y: int = field(init=False) - ... def __attrs_post_init__(self): - ... self.y = self.x + 1 - >>> FrozenBroken(1) - Traceback (most recent call last): - ... - attrs.exceptions.FrozenInstanceError: can't set attribute - -If you need to set attributes on a frozen class, you'll have to resort to the `same trick <how-frozen>` as ``attrs`` and use :meth:`object.__setattr__`: - -.. doctest:: - - >>> @define - ... class Frozen: - ... x: int - ... y: int = field(init=False) - ... def __attrs_post_init__(self): - ... object.__setattr__(self, "y", self.x + 1) - >>> Frozen(1) - Frozen(x=1, y=2) - -Note that you *must not* access the hash code of the object in ``__attrs_post_init__`` if ``cache_hash=True``. - - -Order of Execution ------------------- - -If present, the hooks are executed in the following order: - -1. ``__attrs_pre_init__`` (if present on *current* class) -2. For each attribute, in the order it was declared: - - a. default factory - b. converter - -3. *all* validators -4. ``__attrs_post_init__`` (if present on *current* class) - -Notably this means, that you can access all attributes from within your validators, but your converters have to deal with invalid values and have to return a valid value. - - -Derived Attributes ------------------- - -One of the most common ``attrs`` questions on *Stack Overflow* is how to have attributes that depend on other attributes. -For example if you have an API token and want to instantiate a web client that uses it for authentication. -Based on the previous sections, there's two approaches. - -The simpler one is using ``__attrs_post_init__``:: - - @define - class APIClient: - token: str - client: WebClient = field(init=False) - - def __attrs_post_init__(self): - self.client = WebClient(self.token) - -The second one is using a decorator-based default:: - - @define - class APIClient: - token: str - client: WebClient = field() # needed! attr.ib works too - - @client.default - def _client_factory(self): - return WebClient(self.token) - -That said, and as pointed out in the beginning of the chapter, a better approach would be to have a factory class method:: - - @define - class APIClient: - client: WebClient - - @classmethod - def from_token(cls, token: str) -> SomeClass: - return cls(client=WebClient(token)) - -This makes the class more testable. - - -.. _`Wiki page`: https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs -.. _`get confused`: https://github.com/python-attrs/attrs/issues/289 -.. _`there is no such thing as a private argument`: https://github.com/hynek/characteristic/issues/6 diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/license.md b/tests/wpt/tests/tools/third_party/attrs/docs/license.md new file mode 100644 index 00000000000..aced3448aa9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/license.md @@ -0,0 +1,13 @@ +# License and Credits + +*attrs* is licensed under the [MIT](https://choosealicense.com/licenses/mit/) license. +The full license text can be also found in the [source code repository](https://github.com/python-attrs/attrs/blob/main/LICENSE). + +*attrs* is written and maintained by [Hynek Schlawack](https://hynek.me/). + +The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/), *attrs* [Tidelift subscribers](https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo), and all my amazing [GitHub Sponsors](https://github.com/sponsors/hynek). + +A full list of contributors can be found in [GitHub's overview](https://github.com/python-attrs/attrs/graphs/contributors). + +It’s the spiritual successor of [characteristic](https://characteristic.readthedocs.io/) and aspires to fix some of it clunkiness and unfortunate decisions. +Both were inspired by Twisted’s [FancyEqMixin](https://docs.twisted.org/en/stable/api/twisted.python.util.FancyEqMixin.html) but both are implemented using class decorators because [subclassing is bad for you](https://www.youtube.com/watch?v=3MNVP9-hglc), m’kay? diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/license.rst b/tests/wpt/tests/tools/third_party/attrs/docs/license.rst deleted file mode 100644 index a341a31eb98..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/license.rst +++ /dev/null @@ -1,8 +0,0 @@ -=================== -License and Credits -=================== - -``attrs`` is licensed under the `MIT <https://choosealicense.com/licenses/mit/>`_ license. -The full license text can be also found in the `source code repository <https://github.com/python-attrs/attrs/blob/main/LICENSE>`_. - -.. include:: ../AUTHORS.rst diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/names.md b/tests/wpt/tests/tools/third_party/attrs/docs/names.md new file mode 100644 index 00000000000..c1e9107ba0b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/names.md @@ -0,0 +1,121 @@ +# On The Core API Names + +You may be surprised seeing *attrs* classes being created using {func}`attrs.define` and with type annotated fields, instead of {func}`attr.s` and {func}`attr.ib()`. + +Or, you wonder why the web and talks are full of this weird `attr.s` and `attr.ib` -- including people having strong opinions about it and using `attr.attrs` and `attr.attrib` instead. + +And what even is `attr.dataclass` that's not documented but commonly used!? + + +## TL;DR + +We recommend our modern APIs for new code: + +% {func} syntax does currently not work for `.. function` definitions. + +- {func}`attrs.define` to define a new class, +- [`attrs.mutable()`](attrs.mutable) is an alias for {func}`attrs.define`, +- [`attrs.frozen()`](attrs.frozen) is an alias for `define(frozen=True)` +- and {func}`attrs.field()` to define an attribute. + +They have been added in *attrs* 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. +Sometimes they're referred to as *next-generation* or *NG* APIs. +As of *attrs* 21.3.0 you can also import them from the `attrs` package namespace. + +The traditional APIs {func}`attr.s` / {func}`attr.ib`, their serious-business aliases `attr.attrs` / `attr.attrib`, and the never-documented, but popular `attr.dataclass` easter egg will stay **forever**. + +*attrs* will **never** force you to use type annotations. + + +## A Short History Lesson + +At this point, *attrs* is an old project. +It had its first release in April 2015 -- back when most Python code was on Python 2.7 and Python 3.4 was the first Python 3 release that showed promise. +*attrs* was always Python 3-first, but [type annotations](https://peps.python.org/pep-0484/) came only into Python 3.5 that was released in September 2015 and were largely ignored until years later. + +At this time, if you didn't want to implement all the {term}`dunder methods`, the most common way to create a class with some attributes on it was to subclass a {obj}`collections.namedtuple`, or one of the many hacks that allowed you to access dictionary keys using attribute lookup. + +But *attrs* history goes even a bit further back, to the now-forgotten [*characteristic*](https://github.com/hynek/characteristic) that came out in May 2014 and already used a class decorator, but was overall too unergonomic. + +In the wake of all of that, [Glyph](https://github.com/glyph) and [Hynek](https://github.com/hynek) came together on IRC and brainstormed how to take the good ideas of *characteristic*, but make them easier to use and read. +At this point the plan was not to make *attrs* what it is now -- a flexible class-building kit. +All we wanted was an ergonomic little library to succinctly define classes with attributes. + +Under the impression of the unwieldy `characteristic` name, we went to the other side and decided to make the package name part of the API, and keep the API functions very short. +This led to the infamous {func}`attr.s` and {func}`attr.ib` which some found confusing and pronounced it as "attr dot s" or used a singular `@s` as the decorator. +But it was really just a way to say `attrs` and `attrib`[^attr]. + +[^attr]: We considered calling the PyPI package just `attr` too, but the name was already taken by an *ostensibly* inactive [package on PyPI](https://pypi.org/project/attr/#history). + +Some people hated this cutey API from day one, which is why we added aliases for them that we called *serious business*: `@attr.attrs` and `attr.attrib()`. +Fans of them usually imported the names and didn't use the package name in the first place. +Unfortunately, the `attr` package name started creaking the moment we added `attr.Factory`, since it couldn’t be morphed into something meaningful in any way. +A problem that grew worse over time, as more APIs and even modules were added. + +But overall, *attrs* in this shape was a **huge** success -- especially after Glyph's blog post [*The One Python Library Everyone Needs*](https://glyph.twistedmatrix.com/2016/08/attrs.html) in August 2016 and [*pytest*](https://docs.pytest.org/) adopting it. + +Being able to just write: + +``` +@attr.s +class Point: + x = attr.ib() + y = attr.ib() +``` + +was a big step for those who wanted to write small, focused classes. + +### Dataclasses Enter The Arena + +A big change happened in May 2017 when Hynek sat down with [Guido van Rossum](https://en.wikipedia.org/wiki/Guido_van_Rossum) and [Eric V. Smith](https://github.com/ericvsmith) at PyCon US 2017. + +Type annotations for class attributes have [just landed](https://peps.python.org/pep-0526/) in Python 3.6 and Guido felt like it would be a good mechanic to introduce something similar to *attrs* to the Python standard library. +The result, of course, was {pep}`557`[^stdlib] which eventually became the `dataclasses` module in Python 3.7. + +[^stdlib]: The highly readable PEP also explains why *attrs* wasn't just added to the standard library. + Don't believe the myths and rumors. + +*attrs* at this point was lucky to have several people on board who were also very excited about type annotations and helped implement it; including a [Mypy plugin](https://medium.com/@Pilot-EPD-Blog/mypy-and-attrs-e1b0225e9ac6). +And so it happened that *attrs* [shipped](https://www.attrs.org/en/17.3.0.post2/changelog.html) the new method of defining classes more than half a year before Python 3.7 -- and thus `dataclasses` -- were released. + +--- + +Due to backwards-compatibility concerns, this feature is off by default in the {func}`attr.s` decorator and has to be activated using `@attr.s(auto_attribs=True)`, though. +As a little easter egg and to save ourselves some typing, we've also [added](https://github.com/python-attrs/attrs/commit/88aa1c897dfe2ee4aa987e4a56f2ba1344a17238#diff-4fc63db1f2fcb7c6e464ee9a77c3c74e90dd191d1c9ffc3bdd1234d3a6663dc0R48) an alias called `attr.dataclasses` that just set `auto_attribs=True`. +It was never documented, but people found it and used it and loved it. + +Over the next months and years it became clear that type annotations have become the popular way to define classes and their attributes. +However, it has also become clear that some people viscerally hate type annotations. +We're determined to serve both. + + +### *attrs* TNG + +Over its existence, *attrs* never stood still. +But since we also greatly care about backwards-compatibility and not breaking our users' code, many features and niceties have to be manually activated. + +That is not only annoying, it also leads to the problem that many of *attrs*'s users don't even know what it can do for them. +We've spent years alone explaining that defining attributes using type annotations is in no way unique to {mod}`dataclasses`. + +Finally we've decided to take the [Go route](https://go.dev/blog/module-compatibility): +Instead of fiddling with the old APIs -- whose names felt anachronistic anyway -- we'd define new ones, with better defaults. +So in July 2018, we [looked for better names](https://github.com/python-attrs/attrs/issues/408) and came up with {func}`attr.define`, {func}`attr.field`, and friends. +Then in January 2019, we [started looking for inconvenient defaults](https://github.com/python-attrs/attrs/issues/487) that we now could fix without any repercussions. + +These APIs proved to be very popular, so we've finally changed the documentation to them in November of 2021. + +All of this took way too long, of course. +One reason is the COVID-19 pandemic, but also our fear to fumble this historic chance to fix our APIs. + +Finally, in December 2021, we've added the *attrs* package namespace. + +We hope you like the result: + +``` +from attrs import define + +@define +class Point: + x: int + y: int +``` diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/names.rst b/tests/wpt/tests/tools/third_party/attrs/docs/names.rst deleted file mode 100644 index 0fe953e6a53..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/names.rst +++ /dev/null @@ -1,122 +0,0 @@ -On The Core API Names -===================== - -You may be surprised seeing ``attrs`` classes being created using `attrs.define` and with type annotated fields, instead of `attr.s` and `attr.ib()`. - -Or, you wonder why the web and talks are full of this weird `attr.s` and `attr.ib` -- including people having strong opinions about it and using ``attr.attrs`` and ``attr.attrib`` instead. - -And what even is ``attr.dataclass`` that's not documented but commonly used!? - - -TL;DR ------ - -We recommend our modern APIs for new code: - -- `attrs.define()` to define a new class, -- `attrs.mutable()` is an alias for `attrs.define()`, -- `attrs.frozen()` is an alias for ``define(frozen=True)`` -- and `attrs.field()` to define an attribute. - -They have been added in ``attrs`` 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. -They are only available in Python 3.6 and later. -Sometimes they're referred to as *next-generation* or *NG* APIs. -As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. - -The traditional APIs `attr.s` / `attr.ib`, their serious business aliases ``attr.attrs`` / ``attr.attrib``, and the never-documented, but popular ``attr.dataclass`` easter egg will stay **forever**. - -``attrs`` will **never** force you to use type annotations. - - -A Short History Lesson ----------------------- - -At this point, ``attrs`` is an old project. -It had its first release in April 2015 -- back when most Python code was on Python 2.7 and Python 3.4 was the first Python 3 release that showed promise. -``attrs`` was always Python 3-first, but `type annotations <https://www.python.org/dev/peps/pep-0484/>`_ came only into Python 3.5 that was released in September 2015 and were largely ignored until years later. - -At this time, if you didn't want to implement all the :term:`dunder methods`, the most common way to create a class with some attributes on it was to subclass a `collections.namedtuple`, or one of the many hacks that allowed you to access dictionary keys using attribute lookup. - -But ``attrs`` history goes even a bit further back, to the now-forgotten `characteristic <https://github.com/hynek/characteristic>`_ that came out in May 2014 and already used a class decorator, but was overall too unergonomic. - -In the wake of all of that, `glyph <https://twitter.com/glyph>`_ and `Hynek <https://twitter.com/hynek>`_ came together on IRC and brainstormed how to take the good ideas of ``characteristic``, but make them easier to use and read. -At this point the plan was not to make ``attrs`` what it is now -- a flexible class building kit. -All we wanted was an ergonomic little library to succinctly define classes with attributes. - -Under the impression of of the unwieldy ``characteristic`` name, we went to the other side and decided to make the package name part of the API, and keep the API functions very short. -This led to the infamous `attr.s` and `attr.ib` which some found confusing and pronounced it as "attr dot s" or used a singular ``@s`` as the decorator. -But it was really just a way to say ``attrs`` and ``attrib``\ [#attr]_. - -Some people hated this cutey API from day one, which is why we added aliases for them that we called *serious business*: ``@attr.attrs`` and ``attr.attrib()``. -Fans of them usually imported the names and didn't use the package name in the first place. -Unfortunately, the ``attr`` package name started creaking the moment we added ``attr.Factory``, since it couldn’t be morphed into something meaningful in any way. -A problem that grew worse over time, as more APIs and even modules were added. - -But overall, ``attrs`` in this shape was a **huge** success -- especially after glyph's blog post `The One Python Library Everyone Needs <https://glyph.twistedmatrix.com/2016/08/attrs.html>`_ in August 2016 and `pytest <https://docs.pytest.org/>`_ adopting it. - -Being able to just write:: - - @attr.s - class Point(object): - x = attr.ib() - y = attr.ib() - -was a big step for those who wanted to write small, focused classes. - - -Dataclasses Enter The Arena -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A big change happened in May 2017 when Hynek sat down with `Guido van Rossum <https://en.wikipedia.org/wiki/Guido_van_Rossum>`_ and `Eric V. Smith <https://github.com/ericvsmith>`_ at PyCon US 2017. - -Type annotations for class attributes have `just landed <https://www.python.org/dev/peps/pep-0526/>`_ in Python 3.6 and Guido felt like it would be a good mechanic to introduce something similar to ``attrs`` to the Python standard library. -The result, of course, was `PEP 557 <https://www.python.org/dev/peps/pep-0557/>`_\ [#stdlib]_ which eventually became the `dataclasses` module in Python 3.7. - -``attrs`` at this point was lucky to have several people on board who were also very excited about type annotations and helped implementing it; including a `Mypy plugin <https://medium.com/@Pilot-EPD-Blog/mypy-and-attrs-e1b0225e9ac6>`_. -And so it happened that ``attrs`` `shipped <https://www.attrs.org/en/17.3.0.post2/changelog.html>`_ the new method of defining classes more than half a year before Python 3.7 -- and thus `dataclasses` -- were released. - ------ - -Due to backward-compatibility concerns, this feature is off by default in the `attr.s` decorator and has to be activated using ``@attr.s(auto_attribs=True)``, though. -As a little easter egg and to save ourselves some typing, we've also `added <https://github.com/python-attrs/attrs/commit/88aa1c897dfe2ee4aa987e4a56f2ba1344a17238#diff-4fc63db1f2fcb7c6e464ee9a77c3c74e90dd191d1c9ffc3bdd1234d3a6663dc0R48>`_ an alias called ``attr.dataclasses`` that just set ``auto_attribs=True``. -It was never documented, but people found it and used it and loved it. - -Over the next months and years it became clear that type annotations have become the popular way to define classes and their attributes. -However, it has also become clear that some people viscerally hate type annotations. -We're determined to serve both. - - -``attrs`` TNG -^^^^^^^^^^^^^ - -Over its existence, ``attrs`` never stood still. -But since we also greatly care about backward compatibility and not breaking our users's code, many features and niceties have to be manually activated. - -That is not only annoying, it also leads to the problem that many of ``attrs``'s users don't even know what it can do for them. -We've spent years alone explaining that defining attributes using type annotations is in no way unique to `dataclasses`. - -Finally we've decided to take the `Go route <https://go.dev/blog/module-compatibility>`_: -instead of fiddling with the old APIs -- whose names felt anachronistic anyway -- we'd define new ones, with better defaults. -So in July 2018, we `looked for better names <https://github.com/python-attrs/attrs/issues/408>`_ and came up with `attr.define`, `attr.field`, and friends. -Then in January 2019, we `started looking for inconvenient defaults <https://github.com/python-attrs/attrs/issues/487>`_ that we now could fix without any repercussions. - -These APIs proved to be very popular, so we've finally changed the documentation to them in November of 2021. - -All of this took way too long, of course. -One reason is the COVID-19 pandemic, but also our fear to fumble this historic chance to fix our APIs. - -Finally, in December 2021, we've added the ``attrs`` package namespace. - -We hope you like the result:: - - from attrs import define - - @define - class Point: - x: int - y: int - - -.. [#attr] We considered calling the PyPI package just ``attr`` too, but the name was already taken by an *ostensibly* inactive `package on PyPI <https://pypi.org/project/attr/#history>`_. -.. [#stdlib] The highly readable PEP also explains why ``attrs`` wasn't just added to the standard library. - Don't believe the myths and rumors. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/overview.md b/tests/wpt/tests/tools/third_party/attrs/docs/overview.md new file mode 100644 index 00000000000..5824f70875d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/overview.md @@ -0,0 +1,62 @@ +# Overview + +In order to fulfill its ambitious goal of bringing back the joy to writing classes, it gives you a class decorator and a way to declaratively define the attributes on that class: + +```{include} ../README.md +:start-after: 'code-begin -->' +:end-before: '## Project Information' +``` + + +## Philosophy + +**It's about regular classes.** + +: *attrs* is for creating well-behaved classes with a type, attributes, methods, and everything that comes with a class. + It can be used for data-only containers like `namedtuple`s or `types.SimpleNamespace` but they're just a sub-genre of what *attrs* is good for. + + +**The class belongs to the users.** + +: You define a class and *attrs* adds static methods to that class based on the attributes you declare. + The end. + It doesn't add metaclasses. + It doesn't add classes you've never heard of to your inheritance tree. + An *attrs* class in runtime is indistinguishable from a regular class: because it *is* a regular class with a few boilerplate-y methods attached. + + +**Be light on API impact.** + +: As convenient as it seems at first, *attrs* will *not* tack on any methods to your classes except for the {term}`dunder ones <dunder methods>`. + Hence all the useful [tools](helpers) that come with *attrs* live in functions that operate on top of instances. + Since they take an *attrs* instance as their first argument, you can attach them to your classes with one line of code. + + +**Performance matters.** + +: *attrs* runtime impact is very close to zero because all the work is done when the class is defined. + Once you're instantiating it, *attrs* is out of the picture completely. + + +**No surprises.** + +: *attrs* creates classes that arguably work the way a Python beginner would reasonably expect them to work. + It doesn't try to guess what you mean because explicit is better than implicit. + It doesn't try to be clever because software shouldn't be clever. + +Check out {doc}`how-does-it-work` if you'd like to know how it achieves all of the above. + + +## What *attrs* Is Not + +*attrs* does *not* invent some kind of magic system that pulls classes out of its hat using meta classes, runtime introspection, and shaky interdependencies. + +All *attrs* does is: + +1. Take your declaration, +2. write {term}`dunder methods` based on that information, +3. and attach them to your class. + +It does *nothing* dynamic at runtime, hence zero runtime overhead. +It's still *your* class. +Do with it as you please. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/overview.rst b/tests/wpt/tests/tools/third_party/attrs/docs/overview.rst deleted file mode 100644 index b35f66f2ddc..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/overview.rst +++ /dev/null @@ -1,58 +0,0 @@ -======== -Overview -======== - -In order to fulfill its ambitious goal of bringing back the joy to writing classes, it gives you a class decorator and a way to declaratively define the attributes on that class: - -.. include:: ../README.rst - :start-after: -code-begin- - :end-before: -getting-help- - - -.. _philosophy: - -Philosophy -========== - -**It's about regular classes.** - ``attrs`` is for creating well-behaved classes with a type, attributes, methods, and everything that comes with a class. - It can be used for data-only containers like ``namedtuple``\ s or ``types.SimpleNamespace`` but they're just a sub-genre of what ``attrs`` is good for. - -**The class belongs to the users.** - You define a class and ``attrs`` adds static methods to that class based on the attributes you declare. - The end. - It doesn't add metaclasses. - It doesn't add classes you've never heard of to your inheritance tree. - An ``attrs`` class in runtime is indistinguishable from a regular class: because it *is* a regular class with a few boilerplate-y methods attached. - -**Be light on API impact.** - As convenient as it seems at first, ``attrs`` will *not* tack on any methods to your classes except for the :term:`dunder ones <dunder methods>`. - Hence all the useful `tools <helpers>` that come with ``attrs`` live in functions that operate on top of instances. - Since they take an ``attrs`` instance as their first argument, you can attach them to your classes with one line of code. - -**Performance matters.** - ``attrs`` runtime impact is very close to zero because all the work is done when the class is defined. - Once you're instantiating it, ``attrs`` is out of the picture completely. - -**No surprises.** - ``attrs`` creates classes that arguably work the way a Python beginner would reasonably expect them to work. - It doesn't try to guess what you mean because explicit is better than implicit. - It doesn't try to be clever because software shouldn't be clever. - -Check out `how-does-it-work` if you'd like to know how it achieves all of the above. - - -What ``attrs`` Is Not -===================== - -``attrs`` does *not* invent some kind of magic system that pulls classes out of its hat using meta classes, runtime introspection, and shaky interdependencies. - -All ``attrs`` does is: - -1. take your declaration, -2. write :term:`dunder methods` based on that information, -3. and attach them to your class. - -It does *nothing* dynamic at runtime, hence zero runtime overhead. -It's still *your* class. -Do with it as you please. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/python-2.rst b/tests/wpt/tests/tools/third_party/attrs/docs/python-2.rst deleted file mode 100644 index 7ec9e5112c3..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/python-2.rst +++ /dev/null @@ -1,25 +0,0 @@ -Python 2 Statement -================== - -While ``attrs`` has always been a Python 3-first package, we the maintainers are aware that Python 2 has not magically disappeared in 2020. -We are also aware that ``attrs`` is an important building block in many people's systems and livelihoods. - -As such, we do **not** have any immediate plans to drop Python 2 support in ``attrs``. -We intend to support is as long as it will be technically feasible for us. - -Feasibility in this case means: - -1. Possibility to run the tests on our development computers, -2. and **free** CI options. - -This can mean that we will have to run our tests on PyPy, whose maintainters have unequivocally declared that they do not intend to stop the development and maintenance of their Python 2-compatible line at all. -And this can mean that at some point, a sponsor will have to step up and pay for bespoke CI setups. - -**However**: there is no promise of new features coming to ``attrs`` running under Python 2. -It is up to our discretion alone, to decide whether the introduced complexity or awkwardness are worth it, or whether we choose to make a feature available on modern platforms only. - - -Summary -------- - -We will do our best to support existing users, but nobody is entitled to the latest and greatest features on a platform that is officially end of life. diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/types.md b/tests/wpt/tests/tools/third_party/attrs/docs/types.md new file mode 100644 index 00000000000..5ab7146f6e9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/types.md @@ -0,0 +1,111 @@ +# Type Annotations + +*attrs* comes with first-class support for type annotations for both {pep}`526` and legacy syntax. + +However they will forever remain *optional*, therefore the example from the README could also be written as: + +```{doctest} +>>> from attrs import define, field + +>>> @define +... class SomeClass: +... a_number = field(default=42) +... list_of_numbers = field(factory=list) + +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) +``` + +You can choose freely between the approaches, but please remember that if you choose to use type annotations, you **must** annotate **all** attributes! + +:::{caution} +If you define a class with a {func}`attrs.field` that **lacks** a type annotation, *attrs* will **ignore** other fields that have a type annotation, but are not defined using {func}`attrs.field`: + +```{doctest} +>>> @define +... class SomeClass: +... a_number = field(default=42) +... another_number: int = 23 +>>> SomeClass() +SomeClass(a_number=42) +``` +::: + +Even when going all-in on type annotations, you will need {func}`attrs.field` for some advanced features though. + +One of those features are the decorator-based features like defaults. +It's important to remember that *attrs* doesn't do any magic behind your back. +All the decorators are implemented using an object that is returned by the call to {func}`attrs.field`. + +Attributes that only carry a class annotation do not have that object so trying to call a method on it will inevitably fail. + +--- + +Please note that types -- regardless how added -- are *only metadata* that can be queried from the class and they aren't used for anything out of the box! + +Because Python does not allow references to a class object before the class is defined, +types may be defined as string literals, so-called *forward references* ({pep}`526`). +You can enable this automatically for a whole module by using `from __future__ import annotations` ({pep}`563`) as of Python 3.7. +In this case *attrs* simply puts these string literals into the `type` attributes. +If you need to resolve these to real types, you can call {func}`attrs.resolve_types` which will update the attribute in place. + +In practice though, types show their biggest usefulness in combination with tools like [*Mypy*], [*pytype*], or [*Pyright*] that have dedicated support for *attrs* classes. + +The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you write *correct* and *verified self-documenting* code. + +If you don't know where to start, Carl Meyer gave a great talk on [*Type-checked Python in the Real World*](https://www.youtube.com/watch?v=pMgmKJyWKn8) at PyCon US 2018 that will help you to get started in no time. + + +## Mypy + +While having a nice syntax for type metadata is great, it's even greater that [*Mypy*] as of 0.570 ships with a dedicated *attrs* plugin which allows you to statically check your code. + +Imagine you add another line that tries to instantiate the defined class using `SomeClass("23")`. +Mypy will catch that error for you: + +```console +$ mypy t.py +t.py:12: error: Argument 1 to "SomeClass" has incompatible type "str"; expected "int" +``` + +This happens *without* running your code! + +And it also works with *both* Python 2-style annotation styles. +To *Mypy*, this code is equivalent to the one above: + +```python +@attr.s +class SomeClass: + a_number = attr.ib(default=42) # type: int + list_of_numbers = attr.ib(factory=list, type=list[int]) +``` + + +## Pyright + +*attrs* provides support for [*Pyright*] through the `dataclass_transform` / {pep}`681` specification. +This provides static type inference for a subset of *attrs* equivalent to standard-library {mod}`dataclasses`, +and requires explicit type annotations using the {func}`attrs.define` or `@attr.s(auto_attribs=True)` API. + +Given the following definition, *Pyright* will generate static type signatures for `SomeClass` attribute access, `__init__`, `__eq__`, and comparison methods: + +``` +@attr.define +class SomeClass: + a_number: int = 42 + list_of_numbers: list[int] = attr.field(factory=list) +``` + +:::{warning} +The *Pyright* inferred types are a tiny subset of those supported by *Mypy*, including: + +- The `attrs.frozen` decorator is not typed with frozen attributes, which are properly typed via `attrs.define(frozen=True)`. + +Your constructive feedback is welcome in both [attrs#795](https://github.com/python-attrs/attrs/issues/795) and [pyright#1782](https://github.com/microsoft/pyright/discussions/1782). +Generally speaking, the decision on improving *attrs* support in *Pyright* is entirely Microsoft's prerogative, though. +::: + +[*Mypy*]: http://mypy-lang.org +[*Pyright*]: https://github.com/microsoft/pyright +[*pytype*]: https://google.github.io/pytype/ diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/types.rst b/tests/wpt/tests/tools/third_party/attrs/docs/types.rst deleted file mode 100644 index fbb90a7e930..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/types.rst +++ /dev/null @@ -1,108 +0,0 @@ -Type Annotations -================ - -``attrs`` comes with first class support for type annotations for both Python 3.6 (:pep:`526`) and legacy syntax. - -However they will forever remain *optional*, therefore the example from the README could also be written as: - -.. doctest:: - - >>> from attrs import define, field - - >>> @define - ... class SomeClass: - ... a_number = field(default=42) - ... list_of_numbers = field(factory=list) - - >>> sc = SomeClass(1, [1, 2, 3]) - >>> sc - SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) - -You can choose freely between the approaches, but please remember that if you choose to use type annotations, you **must** annotate **all** attributes! - ----- - -Even when going all-in an type annotations, you will need `attr.field` for some advanced features though. - -One of those features are the decorator-based features like defaults. -It's important to remember that ``attrs`` doesn't do any magic behind your back. -All the decorators are implemented using an object that is returned by the call to `attrs.field`. - -Attributes that only carry a class annotation do not have that object so trying to call a method on it will inevitably fail. - -***** - -Please note that types -- however added -- are *only metadata* that can be queried from the class and they aren't used for anything out of the box! - -Because Python does not allow references to a class object before the class is defined, -types may be defined as string literals, so-called *forward references* (:pep:`526`). -You can enable this automatically for a whole module by using ``from __future__ import annotations`` (:pep:`563`) as of Python 3.7. -In this case ``attrs`` simply puts these string literals into the ``type`` attributes. -If you need to resolve these to real types, you can call `attrs.resolve_types` which will update the attribute in place. - -In practice though, types show their biggest usefulness in combination with tools like mypy_, pytype_, or pyright_ that have dedicated support for ``attrs`` classes. - -The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you writing *correct* and *verified self-documenting* code. - -If you don't know where to start, Carl Meyer gave a great talk on `Type-checked Python in the Real World <https://www.youtube.com/watch?v=pMgmKJyWKn8>`_ at PyCon US 2018 that will help you to get started in no time. - - -mypy ----- - -While having a nice syntax for type metadata is great, it's even greater that mypy_ as of 0.570 ships with a dedicated ``attrs`` plugin which allows you to statically check your code. - -Imagine you add another line that tries to instantiate the defined class using ``SomeClass("23")``. -Mypy will catch that error for you: - -.. code-block:: console - - $ mypy t.py - t.py:12: error: Argument 1 to "SomeClass" has incompatible type "str"; expected "int" - -This happens *without* running your code! - -And it also works with *both* Python 2-style annotation styles. -To mypy, this code is equivalent to the one above: - -.. code-block:: python - - @attr.s - class SomeClass(object): - a_number = attr.ib(default=42) # type: int - list_of_numbers = attr.ib(factory=list, type=list[int]) - - -pyright -------- - -``attrs`` provides support for pyright_ though the dataclass_transform_ specification. -This provides static type inference for a subset of ``attrs`` equivalent to standard-library ``dataclasses``, -and requires explicit type annotations using the `attrs.define` or ``@attr.s(auto_attribs=True)`` API. - -Given the following definition, ``pyright`` will generate static type signatures for ``SomeClass`` attribute access, ``__init__``, ``__eq__``, and comparison methods:: - - @attr.define - class SomeClass: - a_number: int = 42 - list_of_numbers: list[int] = attr.field(factory=list) - -.. warning:: - - The ``pyright`` inferred types are a subset of those supported by ``mypy``, including: - - - The generated ``__init__`` signature only includes the attribute type annotations. - It currently does not include attribute ``converter`` types. - - - The ``attr.frozen`` decorator is not typed with frozen attributes, which are properly typed via ``attr.define(frozen=True)``. - - A `full list <https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md#attrs>`_ of limitations and incompatibilities can be found in pyright's repository. - - Your constructive feedback is welcome in both `attrs#795 <https://github.com/python-attrs/attrs/issues/795>`_ and `pyright#1782 <https://github.com/microsoft/pyright/discussions/1782>`_. - Generally speaking, the decision on improving ``attrs`` support in pyright is entirely Microsoft's prerogative though. - - -.. _mypy: http://mypy-lang.org -.. _pytype: https://google.github.io/pytype/ -.. _pyright: https://github.com/microsoft/pyright -.. _dataclass_transform: https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/why.md b/tests/wpt/tests/tools/third_party/attrs/docs/why.md new file mode 100644 index 00000000000..eeba9db5857 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/docs/why.md @@ -0,0 +1,300 @@ +# Why not… + +If you'd like third party's account why *attrs* is great, have a look at Glyph's [*The One Python Library Everyone Needs*](https://glyph.twistedmatrix.com/2016/08/attrs.html). +It predates type annotations and hence Data Classes, but it masterfully illustrates the appeal of class-building packages. + + +## … Data Classes? + +{pep}`557` added Data Classes to [Python 3.7](https://docs.python.org/3.7/whatsnew/3.7.html#dataclasses) that resemble *attrs* in many ways. + +They are the result of the Python community's [wish](https://mail.python.org/pipermail/python-ideas/2017-May/045618.html) to have an easier way to write classes in the standard library that doesn't carry the problems of `namedtuple`s. +To that end, *attrs* and its developers were involved in the PEP process and while we may disagree with some minor decisions that have been made, it's a fine library and if it stops you from abusing `namedtuple`s, they are a huge win. + +Nevertheless, there are still reasons to prefer *attrs* over Data Classes. +Whether they're relevant to *you* depends on your circumstances: + +- Data Classes are *intentionally* less powerful than *attrs*. + There is a long list of features that were sacrificed for the sake of simplicity and while the most obvious ones are validators, converters, {ref}`equality customization <custom-comparison>`, or {doc}`extensibility <extending>` in general, it permeates throughout all APIs. + + On the other hand, Data Classes currently do not offer any significant feature that *attrs* doesn't already have. + +- *attrs* supports all mainstream Python versions including PyPy. + +- *attrs* doesn't force type annotations on you if you don't like them. + +- But since it **also** supports typing, it's the best way to embrace type hints *gradually*, too. + +- While Data Classes are implementing features from *attrs* every now and then, their presence is dependent on the Python version, not the package version. + For example, support for `__slots__` has only been added in Python 3.10, but it doesn’t do cell rewriting and therefore doesn’t support bare calls to `super()`. + This may or may not be fixed in later Python releases, but handling all these differences is especially painful for PyPI packages that support multiple Python versions. + And of course, this includes possible implementation bugs. + +- *attrs* can and will move faster. + We are not bound to any release schedules and we have a clear deprecation policy. + + One of the [reasons](https://peps.python.org/pep-0557/#why-not-just-use-attrs) to not vendor *attrs* in the standard library was to not impede *attrs*'s future development. + +One way to think about *attrs* vs Data Classes is that *attrs* is a fully-fledged toolkit to write powerful classes while Data Classes are an easy way to get a class with some attributes. +Basically what *attrs* was in 2015. + + +## … Pydantic? + +Pydantic is first and foremost a *data validation & type coercion library*. +As such, it is a capable complement to class building libraries like *attrs* (or Data Classes!) for parsing and validating untrusted data. + +However, as convenient as it might be, using it for your business or data layer [is problematic in several ways](https://threeofwands.com/why-i-use-attrs-instead-of-pydantic/): +Is it really necessary to re-validate all your objects while reading them from a trusted database? +In the parlance of [*Form, Command, and Model Validation*](https://verraes.net/2015/02/form-command-model-validation/), Pydantic is the right tool for *Commands*. + +[*Separation of concerns*](https://en.wikipedia.org/wiki/Separation_of_concerns) feels tedious at times, but it's one of those things that you get to appreciate once you've shot your own foot often enough. + +*attrs* emphatically does **not** try to be a validation library, but a toolkit to write well-behaved classes like you would write yourself. +If you'd like a powerful library for structuring, unstructuring, and validating data, have a look at [*cattrs*](https://catt.rs/) which is an official member of the *attrs* family. + + +## … namedtuples? + +{obj}`collections.namedtuple`s are tuples with names, not classes.[^history] +Since writing classes is tiresome in Python, every now and then someone discovers all the typing they could save and gets really excited. +However, that convenience comes at a price. + +The most obvious difference between `namedtuple`s and *attrs*-based classes is that the latter are type-sensitive: + +```{doctest} +>>> import attrs +>>> C1 = attrs.make_class("C1", ["a"]) +>>> C2 = attrs.make_class("C2", ["a"]) +>>> i1 = C1(1) +>>> i2 = C2(1) +>>> i1.a == i2.a +True +>>> i1 == i2 +False +``` + +…while a `namedtuple` is *intentionally* [behaving like a tuple](https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences) which means the type of a tuple is *ignored*: + +```{doctest} +>>> from collections import namedtuple +>>> NT1 = namedtuple("NT1", "a") +>>> NT2 = namedtuple("NT2", "b") +>>> t1 = NT1(1) +>>> t2 = NT2(1) +>>> t1 == t2 == (1,) +True +``` + +Other often surprising behaviors include: + +- Since they are a subclass of tuples, `namedtuple`s have a length and are both iterable and indexable. + That's not what you'd expect from a class and is likely to shadow subtle typo bugs. + +- Iterability also implies that it's easy to accidentally unpack a `namedtuple` which leads to hard-to-find bugs.[^iter] + +- `namedtuple`s have their methods *on your instances* whether you like it or not.[^pollution] + +- `namedtuple`s are *always* immutable. + Not only does that mean that you can't decide for yourself whether your instances should be immutable or not, it also means that if you want to influence your class' initialization (validation? default values?), you have to implement {meth}`__new__() <object.__new__>` which is a particularly hacky and error-prone requirement for a very common problem.[^immutable] + +- To attach methods to a `namedtuple` you have to subclass it. + And if you follow the standard library documentation's recommendation of: + + ``` + class Point(namedtuple('Point', ['x', 'y'])): + # ... + ``` + + you end up with a class that has *two* `Point`s in its {attr}`__mro__ <class.__mro__>`: `[<class 'point.Point'>, <class 'point.Point'>, <type 'tuple'>, <type 'object'>]`. + + That's not only confusing, it also has very practical consequences: + for example if you create documentation that includes class hierarchies like [*Sphinx*'s autodoc](https://www.sphinx-doc.org/en/stable/usage/extensions/autodoc.html) with `show-inheritance`. + Again: common problem, hacky solution with confusing fallout. + +All these things make `namedtuple`s a particularly poor choice for public APIs because all your objects are irrevocably tainted. +With *attrs* your users won't notice a difference because it creates regular, well-behaved classes. + +:::{admonition} Summary +If you want a *tuple with names*, by all means: go for a `namedtuple`.[^perf] +But if you want a class with methods, you're doing yourself a disservice by relying on a pile of hacks that requires you to employ even more hacks as your requirements expand. + +Other than that, *attrs* also adds nifty features like validators, converters, and (mutable!) default values. +::: + +[^history]: The word is that `namedtuple`s were added to the Python standard library as a way to make tuples in return values more readable. + And indeed that is something you see throughout the standard library. + + Looking at what the makers of `namedtuple`s use it for themselves is a good guideline for deciding on your own use cases. + +[^pollution]: *attrs* only adds a single attribute: `__attrs_attrs__` for introspection. + All helpers are functions in the `attr` package. + Since they take the instance as first argument, you can easily attach them to your classes under a name of your own choice. + +[^iter]: {func}`attrs.astuple` can be used to get that behavior in *attrs* on *explicit demand*. + +[^immutable]: *attrs* offers *optional* immutability through the `frozen` keyword. + +[^perf]: Although *attrs* would serve you just as well! + Since both employ the same method of writing and compiling Python code for you, the performance penalty is negligible at worst and in some cases *attrs* is even faster if you use `slots=True` (which is generally a good idea anyway). + + +## … tuples? + +### Readability + +What makes more sense while debugging: + +``` +Point(x=1, y=2) +``` + +or: + +``` +(1, 2) +``` + +? + +Let's add even more ambiguity: + +``` +Customer(id=42, reseller=23, first_name="Jane", last_name="John") +``` + +or: + +``` +(42, 23, "Jane", "John") +``` + +? + +Why would you want to write `customer[2]` instead of `customer.first_name`? + +Don't get me started when you add nesting. +If you've never run into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter than yours truly. + +Using proper classes with names and types makes program code much more readable and [comprehensible](https://arxiv.org/pdf/1304.5257.pdf). +Especially when trying to grok a new piece of software or returning to old code after several months. + + +### Extendability + +Imagine you have a function that takes or returns a tuple. +Especially if you use tuple unpacking (eg. `x, y = get_point()`), adding additional data means that you have to change the invocation of that function *everywhere*. + +Adding an attribute to a class concerns only those who actually care about that attribute. + + +## … dicts? + +Dictionaries are not for fixed fields. + +If you have a dict, it maps something to something else. +You should be able to add and remove values. + +*attrs* lets you be specific about those expectations; a dictionary does not. +It gives you a named entity (the class) in your code, which lets you explain in other places whether you take a parameter of that class or return a value of that class. + +In other words: if your dict has a fixed and known set of keys, it is an object, not a hash. +So if you never iterate over the keys of a dict, you should use a proper class. + + +## … hand-written classes? + +While we're fans of all things artisanal, writing the same nine methods again and again doesn't qualify. +I usually manage to get some typos inside and there's simply more code that can break and thus has to be tested. + +To bring it into perspective, the equivalent of + +```{doctest} +>>> @attrs.define +... class SmartClass: +... a = attrs.field() +... b = attrs.field() +>>> SmartClass(1, 2) +SmartClass(a=1, b=2) +``` + +is roughly + +```{doctest} +>>> class ArtisanalClass: +... def __init__(self, a, b): +... self.a = a +... self.b = b +... +... def __repr__(self): +... return f"ArtisanalClass(a={self.a}, b={self.b})" +... +... def __eq__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) == (other.a, other.b) +... else: +... return NotImplemented +... +... def __ne__(self, other): +... result = self.__eq__(other) +... if result is NotImplemented: +... return NotImplemented +... else: +... return not result +... +... def __lt__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) < (other.a, other.b) +... else: +... return NotImplemented +... +... def __le__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) <= (other.a, other.b) +... else: +... return NotImplemented +... +... def __gt__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) > (other.a, other.b) +... else: +... return NotImplemented +... +... def __ge__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) >= (other.a, other.b) +... else: +... return NotImplemented +... +... def __hash__(self): +... return hash((self.__class__, self.a, self.b)) +>>> ArtisanalClass(a=1, b=2) +ArtisanalClass(a=1, b=2) +``` + +which is quite a mouthful and it doesn't even use any of *attrs*'s more advanced features like validators or default values. +Also: no tests whatsoever. +And who will guarantee you, that you don't accidentally flip the `<` in your tenth implementation of `__gt__`? + +It also should be noted that *attrs* is not an all-or-nothing solution. +You can freely choose which features you want and disable those that you want more control over: + +```{doctest} +>>> @attrs.define +... class SmartClass: +... a: int +... b: int +... +... def __repr__(self): +... return "<SmartClass(a=%d)>" % (self.a,) +>>> SmartClass(1, 2) +<SmartClass(a=1)> +``` + +:::{admonition} Summary +If you don't care and like typing, we're not gonna stop you. + +However it takes a lot of bias and determined rationalization to claim that *attrs* raises the mental burden on a project given how difficult it is to find the important bits in a hand-written class and how annoying it is to ensure you've copy-pasted your code correctly over all your classes. + +In any case, if you ever get sick of the repetitiveness and the drowning of important code in a sea of boilerplate, *attrs* will be waiting for you. +::: diff --git a/tests/wpt/tests/tools/third_party/attrs/docs/why.rst b/tests/wpt/tests/tools/third_party/attrs/docs/why.rst deleted file mode 100644 index 2c0ca4cd66e..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/docs/why.rst +++ /dev/null @@ -1,290 +0,0 @@ -Why not… -======== - - -If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs <https://glyph.twistedmatrix.com/2016/08/attrs.html>`_! - - -…Data Classes? --------------- - -:pep:`557` added Data Classes to `Python 3.7 <https://docs.python.org/3.7/whatsnew/3.7.html#dataclasses>`_ that resemble ``attrs`` in many ways. - -They are the result of the Python community's `wish <https://mail.python.org/pipermail/python-ideas/2017-May/045618.html>`_ to have an easier way to write classes in the standard library that doesn't carry the problems of ``namedtuple``\ s. -To that end, ``attrs`` and its developers were involved in the PEP process and while we may disagree with some minor decisions that have been made, it's a fine library and if it stops you from abusing ``namedtuple``\ s, they are a huge win. - -Nevertheless, there are still reasons to prefer ``attrs`` over Data Classes. -Whether they're relevant to *you* depends on your circumstances: - -- Data Classes are *intentionally* less powerful than ``attrs``. - There is a long list of features that were sacrificed for the sake of simplicity and while the most obvious ones are validators, converters, :ref:`equality customization <custom-comparison>`, or :doc:`extensibility <extending>` in general, it permeates throughout all APIs. - - On the other hand, Data Classes currently do not offer any significant feature that ``attrs`` doesn't already have. -- ``attrs`` supports all mainstream Python versions, including CPython 2.7 and PyPy. -- ``attrs`` doesn't force type annotations on you if you don't like them. -- But since it **also** supports typing, it's the best way to embrace type hints *gradually*, too. -- While Data Classes are implementing features from ``attrs`` every now and then, their presence is dependent on the Python version, not the package version. - For example, support for ``__slots__`` has only been added in Python 3.10. - That is especially painful for PyPI packages that support multiple Python versions. - This includes possible implementation bugs. -- ``attrs`` can and will move faster. - We are not bound to any release schedules and we have a clear deprecation policy. - - One of the `reasons <https://www.python.org/dev/peps/pep-0557/#why-not-just-use-attrs>`_ to not vendor ``attrs`` in the standard library was to not impede ``attrs``'s future development. - -One way to think about ``attrs`` vs Data Classes is that ``attrs`` is a fully-fledged toolkit to write powerful classes while Data Classes are an easy way to get a class with some attributes. -Basically what ``attrs`` was in 2015. - - -…pydantic? ----------- - -*pydantic* is first an foremost a *data validation library*. -As such, it is a capable complement to class building libraries like ``attrs`` (or Data Classes!) for parsing and validating untrusted data. - -However, as convenient as it might be, using it for your business or data layer `is problematic in several ways <https://threeofwands.com/why-i-use-attrs-instead-of-pydantic/>`_: -Is it really necessary to re-validate all your objects while reading them from a trusted database? -In the parlance of `Form, Command, and Model Validation <https://verraes.net/2015/02/form-command-model-validation/>`_, *pydantic* is the right tool for *Commands*. - -`Separation of concerns <https://en.wikipedia.org/wiki/Separation_of_concerns>`_ feels tedious at times, but it's one of those things that you get to appreciate once you've shot your own foot often enough. - - -…namedtuples? -------------- - -`collections.namedtuple`\ s are tuples with names, not classes. [#history]_ -Since writing classes is tiresome in Python, every now and then someone discovers all the typing they could save and gets really excited. -However, that convenience comes at a price. - -The most obvious difference between ``namedtuple``\ s and ``attrs``-based classes is that the latter are type-sensitive: - -.. doctest:: - - >>> import attr - >>> C1 = attr.make_class("C1", ["a"]) - >>> C2 = attr.make_class("C2", ["a"]) - >>> i1 = C1(1) - >>> i2 = C2(1) - >>> i1.a == i2.a - True - >>> i1 == i2 - False - -…while a ``namedtuple`` is *intentionally* `behaving like a tuple`_ which means the type of a tuple is *ignored*: - -.. doctest:: - - >>> from collections import namedtuple - >>> NT1 = namedtuple("NT1", "a") - >>> NT2 = namedtuple("NT2", "b") - >>> t1 = NT1(1) - >>> t2 = NT2(1) - >>> t1 == t2 == (1,) - True - -Other often surprising behaviors include: - -- Since they are a subclass of tuples, ``namedtuple``\ s have a length and are both iterable and indexable. - That's not what you'd expect from a class and is likely to shadow subtle typo bugs. -- Iterability also implies that it's easy to accidentally unpack a ``namedtuple`` which leads to hard-to-find bugs. [#iter]_ -- ``namedtuple``\ s have their methods *on your instances* whether you like it or not. [#pollution]_ -- ``namedtuple``\ s are *always* immutable. - Not only does that mean that you can't decide for yourself whether your instances should be immutable or not, it also means that if you want to influence your class' initialization (validation? default values?), you have to implement :meth:`__new__() <object.__new__>` which is a particularly hacky and error-prone requirement for a very common problem. [#immutable]_ -- To attach methods to a ``namedtuple`` you have to subclass it. - And if you follow the standard library documentation's recommendation of:: - - class Point(namedtuple('Point', ['x', 'y'])): - # ... - - you end up with a class that has *two* ``Point``\ s in its :attr:`__mro__ <class.__mro__>`: ``[<class 'point.Point'>, <class 'point.Point'>, <type 'tuple'>, <type 'object'>]``. - - That's not only confusing, it also has very practical consequences: - for example if you create documentation that includes class hierarchies like `Sphinx's autodoc <https://www.sphinx-doc.org/en/stable/usage/extensions/autodoc.html>`_ with ``show-inheritance``. - Again: common problem, hacky solution with confusing fallout. - -All these things make ``namedtuple``\ s a particularly poor choice for public APIs because all your objects are irrevocably tainted. -With ``attrs`` your users won't notice a difference because it creates regular, well-behaved classes. - -.. admonition:: Summary - - If you want a *tuple with names*, by all means: go for a ``namedtuple``. [#perf]_ - But if you want a class with methods, you're doing yourself a disservice by relying on a pile of hacks that requires you to employ even more hacks as your requirements expand. - - Other than that, ``attrs`` also adds nifty features like validators, converters, and (mutable!) default values. - - -.. rubric:: Footnotes - -.. [#history] The word is that ``namedtuple``\ s were added to the Python standard library as a way to make tuples in return values more readable. - And indeed that is something you see throughout the standard library. - - Looking at what the makers of ``namedtuple``\ s use it for themselves is a good guideline for deciding on your own use cases. -.. [#pollution] ``attrs`` only adds a single attribute: ``__attrs_attrs__`` for introspection. - All helpers are functions in the ``attr`` package. - Since they take the instance as first argument, you can easily attach them to your classes under a name of your own choice. -.. [#iter] `attr.astuple` can be used to get that behavior in ``attrs`` on *explicit demand*. -.. [#immutable] ``attrs`` offers *optional* immutability through the ``frozen`` keyword. -.. [#perf] Although ``attrs`` would serve you just as well! - Since both employ the same method of writing and compiling Python code for you, the performance penalty is negligible at worst and in some cases ``attrs`` is even faster if you use ``slots=True`` (which is generally a good idea anyway). - -.. _behaving like a tuple: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences - - -…tuples? --------- - -Readability -^^^^^^^^^^^ - -What makes more sense while debugging:: - - Point(x=1, y=2) - -or:: - - (1, 2) - -? - -Let's add even more ambiguity:: - - Customer(id=42, reseller=23, first_name="Jane", last_name="John") - -or:: - - (42, 23, "Jane", "John") - -? - -Why would you want to write ``customer[2]`` instead of ``customer.first_name``? - -Don't get me started when you add nesting. -If you've never run into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter than yours truly. - -Using proper classes with names and types makes program code much more readable and comprehensible_. -Especially when trying to grok a new piece of software or returning to old code after several months. - -.. _comprehensible: https://arxiv.org/pdf/1304.5257.pdf - - -Extendability -^^^^^^^^^^^^^ - -Imagine you have a function that takes or returns a tuple. -Especially if you use tuple unpacking (eg. ``x, y = get_point()``), adding additional data means that you have to change the invocation of that function *everywhere*. - -Adding an attribute to a class concerns only those who actually care about that attribute. - - -…dicts? -------- - -Dictionaries are not for fixed fields. - -If you have a dict, it maps something to something else. -You should be able to add and remove values. - -``attrs`` lets you be specific about those expectations; a dictionary does not. -It gives you a named entity (the class) in your code, which lets you explain in other places whether you take a parameter of that class or return a value of that class. - -In other words: if your dict has a fixed and known set of keys, it is an object, not a hash. -So if you never iterate over the keys of a dict, you should use a proper class. - - -…hand-written classes? ----------------------- - -While we're fans of all things artisanal, writing the same nine methods again and again doesn't qualify. -I usually manage to get some typos inside and there's simply more code that can break and thus has to be tested. - -To bring it into perspective, the equivalent of - -.. doctest:: - - >>> @attr.s - ... class SmartClass(object): - ... a = attr.ib() - ... b = attr.ib() - >>> SmartClass(1, 2) - SmartClass(a=1, b=2) - -is roughly - -.. doctest:: - - >>> class ArtisanalClass(object): - ... def __init__(self, a, b): - ... self.a = a - ... self.b = b - ... - ... def __repr__(self): - ... return "ArtisanalClass(a={}, b={})".format(self.a, self.b) - ... - ... def __eq__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) == (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __ne__(self, other): - ... result = self.__eq__(other) - ... if result is NotImplemented: - ... return NotImplemented - ... else: - ... return not result - ... - ... def __lt__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) < (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __le__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) <= (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __gt__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) > (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __ge__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) >= (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __hash__(self): - ... return hash((self.__class__, self.a, self.b)) - >>> ArtisanalClass(a=1, b=2) - ArtisanalClass(a=1, b=2) - -which is quite a mouthful and it doesn't even use any of ``attrs``'s more advanced features like validators or defaults values. -Also: no tests whatsoever. -And who will guarantee you, that you don't accidentally flip the ``<`` in your tenth implementation of ``__gt__``? - -It also should be noted that ``attrs`` is not an all-or-nothing solution. -You can freely choose which features you want and disable those that you want more control over: - -.. doctest:: - - >>> @attr.s(repr=False) - ... class SmartClass(object): - ... a = attr.ib() - ... b = attr.ib() - ... - ... def __repr__(self): - ... return "<SmartClass(a=%d)>" % (self.a,) - >>> SmartClass(1, 2) - <SmartClass(a=1)> - -.. admonition:: Summary - - If you don't care and like typing, we're not gonna stop you. - - However it takes a lot of bias and determined rationalization to claim that ``attrs`` raises the mental burden on a project given how difficult it is to find the important bits in a hand-written class and how annoying it is to ensure you've copy-pasted your code correctly over all your classes. - - In any case, if you ever get sick of the repetitiveness and drowning important code in a sea of boilerplate, ``attrs`` will be waiting for you. diff --git a/tests/wpt/tests/tools/third_party/attrs/mypy.ini b/tests/wpt/tests/tools/third_party/attrs/mypy.ini deleted file mode 100644 index 685c02599f5..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/mypy.ini +++ /dev/null @@ -1,3 +0,0 @@ -[mypy] -disallow_untyped_defs = True -check_untyped_defs = True diff --git a/tests/wpt/tests/tools/third_party/attrs/pyproject.toml b/tests/wpt/tests/tools/third_party/attrs/pyproject.toml index 52c0e49ec2e..1c72fc26d6a 100644 --- a/tests/wpt/tests/tools/third_party/attrs/pyproject.toml +++ b/tests/wpt/tests/tools/third_party/attrs/pyproject.toml @@ -1,34 +1,157 @@ +# SPDX-License-Identifier: MIT + [build-system] -requires = ["setuptools>=40.6.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme>=23.2.0"] +build-backend = "hatchling.build" + + +[project] +name = "attrs" +authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }] +license = "MIT" +requires-python = ">=3.7" +description = "Classes Without Boilerplate" +keywords = ["class", "attribute", "boilerplate"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed", +] +dependencies = ["importlib_metadata;python_version<'3.8'"] +dynamic = ["version", "readme"] + +[project.optional-dependencies] +tests-mypy = [ + 'pytest-mypy-plugins; python_implementation == "CPython" and python_version >= "3.8"', + # Since the mypy error messages keep changing, we have to keep updating this + # pin. + 'mypy>=1.6; python_implementation == "CPython" and python_version >= "3.8"', +] +tests-no-zope = [ + # For regression test to ensure cloudpickle compat doesn't break. + 'cloudpickle; python_implementation == "CPython"', + "hypothesis", + "pympler", + # 4.3.0 dropped last use of `convert` + "pytest>=4.3.0", + "pytest-xdist[psutil]", + "attrs[tests-mypy]", +] +tests = ["attrs[tests-no-zope]", "zope.interface"] +cov = [ + "attrs[tests]", + # Ensure coverage is new enough for `source_pkgs`. + "coverage[toml]>=5.3", +] +docs = [ + "furo", + "myst-parser", + "sphinx", + "zope.interface", + "sphinx-notfound-page", + "sphinxcontrib-towncrier", + "towncrier", +] +dev = ["attrs[tests]", "pre-commit"] + +[project.urls] +Documentation = "https://www.attrs.org/" +Changelog = "https://www.attrs.org/en/stable/changelog.html" +GitHub = "https://github.com/python-attrs/attrs" +Funding = "https://github.com/sponsors/hynek" +Tidelift = "https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi" + + +[tool.hatch.version] +source = "vcs" +raw-options = { local_scheme = "no-local-version" } + +[tool.hatch.build.targets.wheel] +packages = ["src/attr", "src/attrs"] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +# PyPI doesn't support the <picture> tag. +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """<p align="center"> + <a href="https://www.attrs.org/"> + <img src="https://raw.githubusercontent.com/python-attrs/attrs/main/docs/_static/attrs_logo.svg" width="35%" alt="attrs" /> + </a> +</p> +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" +start-after = "<!-- teaser-begin -->" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ + +## Release Information + +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "CHANGELOG.md" +pattern = "\n(###.+?\n)## " + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ + +--- + +[Full changelog](https://www.attrs.org/en/stable/changelog.html) +""" + +# Point sponsor image URLs to versions. +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +pattern = '\/latest\/_static/sponsors' +replacement = '/$HFPR_VERSION/_static/sponsors' + + +[tool.pytest.ini_options] +addopts = ["-ra", "--strict-markers", "--strict-config"] +xfail_strict = true +testpaths = "tests" +filterwarnings = ["once::Warning", "ignore:::pympler[.*]"] [tool.coverage.run] parallel = true branch = true -source = ["attr", "attrs"] +source_pkgs = ["attr", "attrs"] [tool.coverage.paths] -source = ["src", ".tox/*/site-packages"] +source = ["src", ".tox/py*/**/site-packages"] [tool.coverage.report] show_missing = true +skip_covered = true exclude_lines = [ - "pragma: no cover", - # PyPy is unacceptably slow under coverage. - "if PYPY:", + "pragma: no cover", + # PyPy is unacceptably slow under coverage. + "if PYPY:", + # not meant to be executed + ': \.\.\.$', + '^ +\.\.\.$', ] [tool.black] line-length = 79 -extend-exclude = ''' -# Exclude pattern matching test till black gains Python 3.10 support -.*test_pattern_matching.* -''' [tool.interrogate] +omit-covered-files = true verbose = 2 fail-under = 100 whitelist-regex = ["test_.*"] @@ -38,34 +161,113 @@ whitelist-regex = ["test_.*"] toplevel = ["attr", "attrs"] -[tool.isort] -profile = "attrs" +[tool.ruff] +src = ["src", "tests", "conftest.py", "docs"] + +select = ["ALL"] +ignore = [ + "A001", # shadowing is fine + "A002", # shadowing is fine + "A003", # shadowing is fine + "ANN", # Mypy is better at this + "ARG", # unused arguments are normal when implementing interfaces + "COM", # Black takes care of our commas + "D", # We prefer our own docstring style. + "E501", # leave line-length enforcement to Black + "FBT", # we don't hate bool args around here + "FIX", # Yes, we want XXX as a marker. + "PLR0913", # yes, many arguments, but most have defaults + "PLR2004", # numbers are sometimes fine + "SLF001", # private members are accessed by friendly functions + "TCH", # TYPE_CHECKING blocks break autodocs + "TD", # we don't follow other people's todo style + "C901", # we're complex software + "PLR0911", # we're complex software + "PLR0912", # we're complex software + "PLR0915", # we're complex software + "PGH001", # eval FTW + "S307", # eval FTW + "N807", # we need to create functions that start with __ + "ERA001", # we need to keep around some notes + "RSE102", # I like empty parens on raised exceptions + "N", # we need more naming freedom + "UP031", # format() is slow as molasses; % and f'' FTW. + "PD", # we're not pandas + "PLW0603", # sometimes we need globals + "TRY301", # I'm sorry, but this makes not sense for us. +] + +[tool.ruff.per-file-ignores] +"**/test_*" = [ + "ARG005", # we need stub lambdas + "S", + "SIM300", # Yoda rocks in asserts + "SIM201", # sometimes we need to check `not ==` + "SIM202", # sometimes we need to check `not ==` + "PT005", # we always add underscores and explicit names + "PT011", # broad is fine + "TRY", # exception best practices don't matter in tests + "EM101", # no need for exception msg hygiene in tests + "B904", # exception best practices don't matter in tests + "B015", # pointless comparison in tests aren't pointless + "B018", # pointless expressions in tests aren't pointless + "PLR0124", # pointless comparison in tests aren't pointless + "DTZ", # datetime best practices don't matter in tests + "UP037", # we test some older syntaxes on purpose + "B017", # pytest.raises(Exception) is fine + "PT012", # sometimes we need more than a single stmt + "RUF012", # we don't do ClassVar annotations in tests +] + +"conftest.py" = [ + "PT005", # we always add underscores and explicit names +] + +"src/*/*.pyi" = ["ALL"] # TODO +"tests/test_annotations.py" = ["FA100"] +"tests/typing_example.py" = [ + "E741", # ambiguous variable names don't matter in type checks + "B018", # useless expressions aren't useless in type checks + "B015", # pointless comparison in type checks aren't pointless + "TRY301", # exception hygiene isn't important in type checks + "UP037", # we test some older syntaxes on purpose +] + +[tool.ruff.isort] +lines-between-types = 1 +lines-after-imports = 2 +known-first-party = ["attr", "attrs"] [tool.towncrier] - package = "attr" - package_dir = "src" - filename = "CHANGELOG.rst" - template = "changelog.d/towncrier_template.rst" - issue_format = "`#{issue} <https://github.com/python-attrs/attrs/issues/{issue}>`_" - directory = "changelog.d" - title_format = "{version} ({project_date})" - underlines = ["-", "^"] - - [[tool.towncrier.section]] - path = "" - - [[tool.towncrier.type]] - directory = "breaking" - name = "Backward-incompatible Changes" - showcontent = true - - [[tool.towncrier.type]] - directory = "deprecation" - name = "Deprecations" - showcontent = true - - [[tool.towncrier.type]] - directory = "change" - name = "Changes" - showcontent = true +name = "attrs" +directory = "changelog.d" +filename = "CHANGELOG.md" +start_string = "<!-- towncrier release notes start -->\n" +template = "changelog.d/towncrier_template.md.jinja" +title_format = "" +issue_format = "[#{issue}](https://github.com/python-attrs/attrs/issues/{issue})" +underlines = ["", "", ""] + +[[tool.towncrier.section]] +path = "" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Backwards-incompatible Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecations" +showcontent = true + +[[tool.towncrier.type]] +directory = "change" +name = "Changes" +showcontent = true + + +[tool.mypy] +disallow_untyped_defs = true +check_untyped_defs = true diff --git a/tests/wpt/tests/tools/third_party/attrs/setup.py b/tests/wpt/tests/tools/third_party/attrs/setup.py deleted file mode 100644 index 00e7b012ae7..00000000000 --- a/tests/wpt/tests/tools/third_party/attrs/setup.py +++ /dev/null @@ -1,151 +0,0 @@ -# SPDX-License-Identifier: MIT - -import codecs -import os -import platform -import re -import sys - -from setuptools import find_packages, setup - - -############################################################################### - -NAME = "attrs" -PACKAGES = find_packages(where="src") -META_PATH = os.path.join("src", "attr", "__init__.py") -KEYWORDS = ["class", "attribute", "boilerplate"] -PROJECT_URLS = { - "Documentation": "https://www.attrs.org/", - "Changelog": "https://www.attrs.org/en/stable/changelog.html", - "Bug Tracker": "https://github.com/python-attrs/attrs/issues", - "Source Code": "https://github.com/python-attrs/attrs", - "Funding": "https://github.com/sponsors/hynek", - "Tidelift": "https://tidelift.com/subscription/pkg/pypi-attrs?" - "utm_source=pypi-attrs&utm_medium=pypi", - "Ko-fi": "https://ko-fi.com/the_hynek", -} -CLASSIFIERS = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Natural Language :: English", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries :: Python Modules", -] -INSTALL_REQUIRES = [] -EXTRAS_REQUIRE = { - "docs": ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"], - "tests_no_zope": [ - # For regression test to ensure cloudpickle compat doesn't break. - 'cloudpickle; python_implementation == "CPython"', - # 5.0 introduced toml; parallel was broken until 5.0.2 - "coverage[toml]>=5.0.2", - "hypothesis", - "pympler", - "pytest>=4.3.0", # 4.3.0 dropped last use of `convert` - "six", - ], -} -if ( - sys.version_info[:2] >= (3, 6) - and platform.python_implementation() != "PyPy" -): - EXTRAS_REQUIRE["tests_no_zope"].extend(["mypy", "pytest-mypy-plugins"]) - -EXTRAS_REQUIRE["tests"] = EXTRAS_REQUIRE["tests_no_zope"] + ["zope.interface"] -EXTRAS_REQUIRE["dev"] = ( - EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"] -) - -############################################################################### - -HERE = os.path.abspath(os.path.dirname(__file__)) - - -def read(*parts): - """ - Build an absolute path from *parts* and return the contents of the - resulting file. Assume UTF-8 encoding. - """ - with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: - return f.read() - - -META_FILE = read(META_PATH) - - -def find_meta(meta): - """ - Extract __*meta*__ from META_FILE. - """ - meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), META_FILE, re.M - ) - if meta_match: - return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) - - -LOGO = """ -.. image:: https://www.attrs.org/en/stable/_static/attrs_logo.png - :alt: attrs logo - :align: center -""" # noqa - -VERSION = find_meta("version") -URL = find_meta("url") -LONG = ( - LOGO - + read("README.rst").split(".. teaser-begin")[1] - + "\n\n" - + "Release Information\n" - + "===================\n\n" - + re.search( - r"(\d+.\d.\d \(.*?\)\r?\n.*?)\r?\n\r?\n\r?\n----\r?\n\r?\n\r?\n", - read("CHANGELOG.rst"), - re.S, - ).group(1) - + "\n\n`Full changelog " - + "<{url}en/stable/changelog.html>`_.\n\n".format(url=URL) - + read("AUTHORS.rst") -) - - -if __name__ == "__main__": - setup( - name=NAME, - description=find_meta("description"), - license=find_meta("license"), - url=URL, - project_urls=PROJECT_URLS, - version=VERSION, - author=find_meta("author"), - author_email=find_meta("email"), - maintainer=find_meta("author"), - maintainer_email=find_meta("email"), - keywords=KEYWORDS, - long_description=LONG, - long_description_content_type="text/x-rst", - packages=PACKAGES, - package_dir={"": "src"}, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - zip_safe=False, - classifiers=CLASSIFIERS, - install_requires=INSTALL_REQUIRES, - extras_require=EXTRAS_REQUIRE, - include_package_data=True, - options={"bdist_wheel": {"universal": "1"}}, - ) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.py index f95c96dd579..9226258a2d5 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.py @@ -1,13 +1,15 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function - -import sys +""" +Classes Without Boilerplate +""" from functools import partial +from typing import Callable from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using +from ._compat import Protocol from ._config import get_run_validators, set_run_validators from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types from ._make import ( @@ -21,31 +23,22 @@ from ._make import ( make_class, validate, ) +from ._next_gen import define, field, frozen, mutable from ._version_info import VersionInfo -__version__ = "21.4.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - s = attributes = attrs ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +class AttrsInstance(Protocol): + pass + + __all__ = [ "Attribute", + "AttrsInstance", "Factory", "NOTHING", "asdict", @@ -57,15 +50,19 @@ __all__ = [ "attrs", "cmp_using", "converters", + "define", "evolve", "exceptions", + "field", "fields", "fields_dict", "filters", + "frozen", "get_run_validators", "has", "ib", "make_class", + "mutable", "resolve_types", "s", "set_run_validators", @@ -74,7 +71,64 @@ __all__ = [ "validators", ] -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - __all__.extend(("define", "field", "frozen", "mutable")) +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + dunder_to_metadata = { + "__title__": "Name", + "__copyright__": "", + "__version__": "version", + "__version_info__": "version", + "__description__": "summary", + "__uri__": "", + "__url__": "", + "__author__": "", + "__email__": "", + "__license__": "license", + } + if name not in dunder_to_metadata: + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + import sys + import warnings + + if sys.version_info < (3, 8): + from importlib_metadata import metadata + else: + from importlib.metadata import metadata + + if name not in ("__version__", "__version_info__"): + warnings.warn( + f"Accessing {mod_name}.{name} is deprecated and will be " + "removed in a future release. Use importlib.metadata directly " + "to query for attrs's packaging metadata.", + DeprecationWarning, + stacklevel=2, + ) + + meta = metadata("attrs") + if name == "__license__": + return "MIT" + if name == "__copyright__": + return "Copyright (c) 2015 Hynek Schlawack" + if name in ("__uri__", "__url__"): + return meta["Project-URL"].split(" ", 1)[-1] + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + if name == "__author__": + return meta["Author-email"].rsplit(" ", 1)[0] + if name == "__email__": + return meta["Author-email"].rsplit("<", 1)[1][:-1] + + return meta[dunder_to_metadata[name]] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.pyi index c0a21265036..37a208732ac 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/__init__.pyi @@ -1,3 +1,4 @@ +import enum import sys from typing import ( @@ -8,6 +9,7 @@ from typing import ( List, Mapping, Optional, + Protocol, Sequence, Tuple, Type, @@ -22,8 +24,20 @@ from . import exceptions as exceptions from . import filters as filters from . import setters as setters from . import validators as validators +from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ from ._version_info import VersionInfo +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + __version__: str __version_info__: VersionInfo __title__: str @@ -39,27 +53,33 @@ _T = TypeVar("_T") _C = TypeVar("_C", bound=type) _EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] _ConverterType = Callable[[Any], Any] -_FilterType = Callable[[Attribute[_T], _T], bool] +_FilterType = Callable[["Attribute[_T]", _T], bool] _ReprType = Callable[[Any], str] _ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] _OnSetAttrArgType = Union[ _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType ] _FieldTransformer = Callable[ - [type, List[Attribute[Any]]], List[Attribute[Any]] + [type, List["Attribute[Any]"]], List["Attribute[Any]"] ] -_CompareWithType = Callable[[Any, Any], bool] # FIXME: in reality, if multiple validators are passed they must be in a list # or tuple, but those are invariant and so would prevent subtypes of # _ValidatorType from working when passed in a list or tuple. _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] -# _make -- +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass -NOTHING: object +_A = TypeVar("_A", bound=type[AttrsInstance]) + +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING # NOTE: Factory lies about its return type to make this possible: # `x: List[int] # = Factory(list)` @@ -88,22 +108,6 @@ else: takes_self: bool = ..., ) -> _T: ... -# Static type inference support via __dataclass_transform__ implemented as per: -# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md -# This annotation must be applied to all overloads of "define" and "attrs" -# -# NOTE: This is a typing construct and does not exist at runtime. Extensions -# wrapping attrs decorators should declare a separate __dataclass_transform__ -# signature in the extension module using the specification linked above to -# provide pyright support. -def __dataclass_transform__( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), -) -> Callable[[_T], _T]: ... - class Attribute(Generic[_T]): name: str default: Optional[_T] @@ -119,6 +123,8 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] kw_only: bool on_setattr: _OnSetAttrType + alias: Optional[str] + def evolve(self, **changes: Any) -> "Attribute[Any]": ... # NOTE: We had several choices for the annotation to use for type arg: @@ -161,6 +167,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -181,6 +188,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -200,6 +208,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -219,6 +228,7 @@ def attrib( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., ) -> Any: ... @overload def field( @@ -235,6 +245,8 @@ def field( eq: Optional[bool] = ..., order: Optional[bool] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... # This form catches an explicit None or no default and infers the type from the @@ -254,6 +266,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form catches an explicit default argument. @@ -272,6 +286,8 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> _T: ... # This form covers type=non-Type: e.g. forward references (str), Any @@ -290,9 +306,11 @@ def field( eq: Optional[_EqOrderType] = ..., order: Optional[_EqOrderType] = ..., on_setattr: Optional[_OnSetAttrArgType] = ..., + alias: Optional[str] = ..., + type: Optional[type] = ..., ) -> Any: ... @overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., @@ -317,9 +335,10 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> _C: ... @overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) def attrs( maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., @@ -344,14 +363,16 @@ def attrs( on_setattr: Optional[_OnSetAttrArgType] = ..., field_transformer: Optional[_FieldTransformer] = ..., match_args: bool = ..., + unsafe_hash: Optional[bool] = ..., ) -> Callable[[_C], _C]: ... @overload -@__dataclass_transform__(field_descriptors=(attrib, field)) +@dataclass_transform(field_specifiers=(attrib, field)) def define( maybe_cls: _C, *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -371,12 +392,13 @@ def define( match_args: bool = ..., ) -> _C: ... @overload -@__dataclass_transform__(field_descriptors=(attrib, field)) +@dataclass_transform(field_specifiers=(attrib, field)) def define( maybe_cls: None = ..., *, these: Optional[Dict[str, Any]] = ..., repr: bool = ..., + unsafe_hash: Optional[bool] = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., @@ -397,21 +419,69 @@ def define( ) -> Callable[[_C], _C]: ... mutable = define -frozen = define # they differ only in their defaults - -# TODO: add support for returning NamedTuple from the mypy plugin -class _Fields(Tuple[Attribute[Any], ...]): - def __getattr__(self, name: str) -> Attribute[Any]: ... -def fields(cls: type) -> _Fields: ... -def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... -def validate(inst: Any) -> None: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + unsafe_hash: Optional[bool] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... +def fields(cls: Type[AttrsInstance]) -> Any: ... +def fields_dict(cls: Type[AttrsInstance]) -> Dict[str, Attribute[Any]]: ... +def validate(inst: AttrsInstance) -> None: ... def resolve_types( - cls: _C, + cls: _A, globalns: Optional[Dict[str, Any]] = ..., localns: Optional[Dict[str, Any]] = ..., attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... + include_extras: bool = ..., +) -> _A: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', @@ -420,6 +490,7 @@ def make_class( name: str, attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], bases: Tuple[type, ...] = ..., + class_body: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: Optional[_EqOrderType] = ..., @@ -449,7 +520,7 @@ def make_class( # https://github.com/python/typing/issues/253 # XXX: remember to fix attrs.asdict/astuple too! def asdict( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., dict_factory: Type[Mapping[Any, Any]] = ..., @@ -462,13 +533,13 @@ def asdict( # TODO: add support for returning NamedTuple from the mypy plugin def astuple( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., tuple_factory: Type[Sequence[Any]] = ..., retain_collection_types: bool = ..., ) -> Tuple[Any, ...]: ... -def has(cls: type) -> bool: ... +def has(cls: type) -> TypeGuard[Type[AttrsInstance]]: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.py index 6cffa4dbabd..a4a35e08fc9 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.py @@ -1,10 +1,9 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function import functools +import types -from ._compat import new_class from ._make import _make_ne @@ -21,22 +20,22 @@ def cmp_using( class_name="Comparable", ): """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. - - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + :param Optional[callable] eq: `callable` used to evaluate equality of two + objects. + :param Optional[callable] lt: `callable` used to evaluate whether one + object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether one + object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether one + object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether one + object is greater than or equal to another object. :param bool require_same_type: When `True`, equality and ordering methods will return `NotImplemented` if objects are not of the same type. @@ -80,7 +79,9 @@ def cmp_using( num_order_functions += 1 body["__ge__"] = _make_operator("ge", ge) - type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) + type_ = types.new_class( + class_name, (object,), {}, lambda ns: ns.update(body) + ) # Add same type requirement. if require_same_type: @@ -91,10 +92,8 @@ def cmp_using( if not has_eq_function: # functools.total_ordering requires __eq__ to be defined, # so raise early error here to keep a nice stack. - raise ValueError( - "eq must be define is order to complete ordering from " - "lt, le, gt, ge." - ) + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) type_ = functools.total_ordering(type_) return type_ @@ -129,9 +128,9 @@ def _make_operator(name, func): return result - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." ) return method @@ -141,10 +140,7 @@ def _is_comparable_to(self, other): """ Check whether `other` is comparable to `self`. """ - for func in self._requirements: - if not func(self, other): - return False - return True + return all(func(self, other) for func in self._requirements) def _check_same_type(self, other): diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.pyi index e71aaff7a19..f3dcdc1a754 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_cmp.pyi @@ -1,13 +1,13 @@ -from typing import Type +from typing import Any, Callable, Optional, Type -from . import _CompareWithType +_CompareWithType = Callable[[Any, Any], bool] def cmp_using( - eq: Optional[_CompareWithType], - lt: Optional[_CompareWithType], - le: Optional[_CompareWithType], - gt: Optional[_CompareWithType], - ge: Optional[_CompareWithType], - require_same_type: bool, - class_name: str, + eq: Optional[_CompareWithType] = ..., + lt: Optional[_CompareWithType] = ..., + le: Optional[_CompareWithType] = ..., + gt: Optional[_CompareWithType] = ..., + ge: Optional[_CompareWithType] = ..., + require_same_type: bool = ..., + class_name: str = ..., ) -> Type: ... diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_compat.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_compat.py index dc0cb02b643..46b05ca4537 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_compat.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_compat.py @@ -1,250 +1,69 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function - +import inspect import platform import sys import threading -import types -import warnings + +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias -PY2 = sys.version_info[0] == 2 PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 +PY_3_8_PLUS = sys.version_info[:2] >= (3, 8) +PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) PY310 = sys.version_info[:2] >= (3, 10) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) -if PYPY or PY36: - ordered_dict = dict +if sys.version_info < (3, 8): + try: + from typing_extensions import Protocol + except ImportError: # pragma: no cover + Protocol = object else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - -if PY2: - from collections import Mapping, Sequence + from typing import Protocol # noqa: F401 - from UserDict import IterableUserDict - # We 'bundle' isclass instead of using inspect as importing inspect is - # fairly expensive (order of 10-15 ms for a modern machine in 2016) - def isclass(klass): - return isinstance(klass, (type, types.ClassType)) - - def new_class(name, bases, kwds, exec_body): - """ - A minimal stub of types.new_class that we need for make_class. - """ - ns = {} - exec_body(ns) - - return type(name, bases, ns) +class _AnnotationExtractor: + """ + Extract type annotations from a callable, returning None whenever there + is none. + """ - # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. - TYPE = "type" + __slots__ = ["sig"] - def iteritems(d): - return d.iteritems() + def __init__(self, callable): + try: + self.sig = inspect.signature(callable) + except (ValueError, TypeError): # inspect failed + self.sig = None - # Python 2 is bereft of a read-only dict proxy, so we make one! - class ReadOnlyDict(IterableUserDict): + def get_first_param_type(self): """ - Best-effort read-only dict wrapper. + Return the type annotation of the first argument if it's not empty. """ + if not self.sig: + return None - def __setitem__(self, key, val): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item assignment" - ) + params = list(self.sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + return params[0].annotation - def update(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'update'" - ) + return None - def __delitem__(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item deletion" - ) - - def clear(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'clear'" - ) - - def pop(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'pop'" - ) - - def popitem(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'popitem'" - ) - - def setdefault(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'setdefault'" - ) - - def __repr__(self): - # Override to be identical to the Python 3 version. - return "mappingproxy(" + repr(self.data) + ")" - - def metadata_proxy(d): - res = ReadOnlyDict() - res.data.update(d) # We blocked update, so we have to do it like this. - return res - - def just_warn(*args, **kw): # pragma: no cover - """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. - """ - -else: # Python 3 and later. - from collections.abc import Mapping, Sequence # noqa - - def just_warn(*args, **kw): + def get_return_type(self): """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. + Return the return type if it's not empty. """ - warnings.warn( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - RuntimeWarning, - stacklevel=2, - ) - - def isclass(klass): - return isinstance(klass, type) + if ( + self.sig + and self.sig.return_annotation is not inspect.Signature.empty + ): + return self.sig.return_annotation - TYPE = "class" + return None - def iteritems(d): - return d.items() - - new_class = types.new_class - - def metadata_proxy(d): - return types.MappingProxyType(dict(d)) - - -def make_set_closure_cell(): - """Return a function of two arguments (cell, value) which sets - the value stored in the closure cell `cell` to `value`. - """ - # pypy makes this easy. (It also supports the logic below, but - # why not do the easy/fast thing?) - if PYPY: - - def set_closure_cell(cell, value): - cell.__setstate__((value,)) - - return set_closure_cell - - # Otherwise gotta do it the hard way. - - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - if PY2: - co = set_first_cellvar_to.func_code - else: - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. - if sys.version_info >= (3, 8): - # CPython 3.8+ has an incompatible CodeType signature - # (added a posonlyargcount argument) but also added - # CodeType.replace() to do this without counting parameters. - set_first_freevar_code = co.replace( - co_cellvars=co.co_freevars, co_freevars=co.co_cellvars - ) - else: - args = [co.co_argcount] - if not PY2: - args.append(co.co_kwonlyargcount) - args.extend( - [ - co.co_nlocals, - co.co_stacksize, - co.co_flags, - co.co_code, - co.co_consts, - co.co_names, - co.co_varnames, - co.co_filename, - co.co_name, - co.co_firstlineno, - co.co_lnotab, - # These two arguments are reversed: - co.co_cellvars, - co.co_freevars, - ] - ) - set_first_freevar_code = types.CodeType(*args) - - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) - - # Make sure it works on this interpreter: - def make_func_with_cell(): - x = None - - def func(): - return x # pragma: no cover - - return func - - if PY2: - cell = make_func_with_cell().func_closure[0] - else: - cell = make_func_with_cell().__closure__[0] - set_closure_cell(cell, 100) - if cell.cell_contents != 100: - raise AssertionError # pragma: no cover - - except Exception: - return just_warn - else: - return set_closure_cell - - -set_closure_cell = make_set_closure_cell() # Thread-local global to track attrs instances which are already being repr'd. # This is needed because there is no other (thread-safe) way to pass info @@ -259,3 +78,10 @@ set_closure_cell = make_set_closure_cell() # don't have a direct reference to the thread-local in their globals dict. # If they have such a reference, it breaks cloudpickle. repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_config.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_config.py index fc9be29d008..9c245b1461a 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_config.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_config.py @@ -1,8 +1,5 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function - - __all__ = ["set_run_validators", "get_run_validators"] _run_validators = True @@ -17,7 +14,8 @@ def set_run_validators(run): instead. """ if not isinstance(run, bool): - raise TypeError("'run' must be bool.") + msg = "'run' must be bool." + raise TypeError(msg) global _run_validators _run_validators = run diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_funcs.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_funcs.py index 4c90085a401..a888991d98f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_funcs.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_funcs.py @@ -1,10 +1,9 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function import copy -from ._compat import iteritems +from ._compat import PY_3_9_PLUS, get_generic_base from ._make import NOTHING, _obj_setattr, fields from .exceptions import AttrsAttributeNotFoundError @@ -18,13 +17,13 @@ def asdict( value_serializer=None, ): """ - Return the ``attrs`` attribute values of *inst* as a dict. + Return the *attrs* attribute values of *inst* as a dict. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -42,7 +41,7 @@ def asdict( :rtype: return type of *dict_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.0.0 *dict_factory* @@ -73,19 +72,25 @@ def asdict( ) elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain_collection_types is True else list - rv[a.name] = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in v - ] - ) + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) elif isinstance(v, dict): df = dict_factory rv[a.name] = df( @@ -107,7 +112,7 @@ def asdict( value_serializer=value_serializer, ), ) - for kk, vv in iteritems(v) + for kk, vv in v.items() ) else: rv[a.name] = v @@ -179,7 +184,7 @@ def _asdict_anything( value_serializer=value_serializer, ), ) - for kk, vv in iteritems(val) + for kk, vv in val.items() ) else: rv = val @@ -197,13 +202,13 @@ def astuple( retain_collection_types=False, ): """ - Return the ``attrs`` attribute values of *inst* as a tuple. + Return the *attrs* attribute values of *inst* as a tuple. - Optionally recurse into other ``attrs``-decorated classes. + Optionally recurse into other *attrs*-decorated classes. - :param inst: Instance of an ``attrs``-decorated class. + :param inst: Instance of an *attrs*-decorated class. :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. + *attrs*-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is called with the `attrs.Attribute` as the first argument and the @@ -217,7 +222,7 @@ def astuple( :rtype: return type of *tuple_factory* - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 16.2.0 @@ -242,22 +247,26 @@ def astuple( ) elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain is True else list - rv.append( - cf( - [ - astuple( - j, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(j.__class__) - else j - for j in v - ] + items = [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, ) - ) + if has(j.__class__) + else j + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) elif isinstance(v, dict): df = v.__class__ if retain is True else dict rv.append( @@ -278,7 +287,7 @@ def astuple( if has(vv.__class__) else vv, ) - for kk, vv in iteritems(v) + for kk, vv in v.items() ) ) else: @@ -291,28 +300,48 @@ def astuple( def has(cls): """ - Check whether *cls* is a class with ``attrs`` attributes. + Check whether *cls* is a class with *attrs* attributes. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. :rtype: bool """ - return getattr(cls, "__attrs_attrs__", None) is not None + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False def assoc(inst, **changes): """ Copy *inst* and apply *changes*. - :param inst: Instance of a class with ``attrs`` attributes. + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name* + couldn't be found on *cls*. + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. deprecated:: 17.1.0 @@ -320,57 +349,79 @@ def assoc(inst, **changes): This function will not be removed du to the slightly different approach compared to `attrs.evolve`. """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) new = copy.copy(inst) attrs = fields(inst.__class__) - for k, v in iteritems(changes): + for k, v in changes.items(): a = getattr(attrs, k, NOTHING) if a is NOTHING: - raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) - ) + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) _obj_setattr(new, k, v) return new -def evolve(inst, **changes): +def evolve(*args, **changes): """ - Create a new instance, based on *inst* with *changes* applied. + Create a new instance, based on the first positional argument with + *changes* applied. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. :param changes: Keyword changes in the new copy. :return: A copy of inst with *changes* incorporated. :raise TypeError: If *attr_name* couldn't be found in the class ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - .. versionadded:: 17.1.0 + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. """ + # Try to get instance by positional argument first. + # Use changes otherwise and warn it'll break. + if args: + try: + (inst,) = args + except ValueError: + msg = f"evolve() takes 1 positional argument, but {len(args)} were given" + raise TypeError(msg) from None + else: + try: + inst = changes.pop("inst") + except KeyError: + msg = "evolve() missing 1 required positional argument: 'inst'" + raise TypeError(msg) from None + + import warnings + + warnings.warn( + "Passing the instance per keyword argument is deprecated and " + "will stop working in, or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) + cls = inst.__class__ attrs = fields(cls) for a in attrs: if not a.init: continue attr_name = a.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + init_name = a.alias if init_name not in changes: changes[init_name] = getattr(inst, attr_name) return cls(**changes) -def resolve_types(cls, globalns=None, localns=None, attribs=None): +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): """ Resolve any strings and forward annotations in type annotations. @@ -389,10 +440,14 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): :param Optional[dict] localns: Dictionary containing local variables. :param Optional[list] attribs: List of attribs for the given class. This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. + since *cls* is not an *attrs* class yet. + :param bool include_extras: Resolve more accurately, if possible. + Pass ``include_extras`` to ``typing.get_hints``, if supported by the + typing module. On supported Python versions (3.9+), this resolves the + types more accurately. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. @@ -402,6 +457,7 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* """ # Since calling get_type_hints is expensive we cache whether we've @@ -409,7 +465,12 @@ def resolve_types(cls, globalns=None, localns=None, attribs=None): if getattr(cls, "__attrs_types_resolved__", None) != cls: import typing - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + kwargs = {"globalns": globalns, "localns": localns} + + if PY_3_9_PLUS: + kwargs["include_extras"] = include_extras + + hints = typing.get_type_hints(cls, **kwargs) for field in fields(cls) if attribs is None else attribs: if field.name in hints: # Since fields have been frozen we must work around it. diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_make.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_make.py index d46f8a3e7a4..10b4eca7796 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_make.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_make.py @@ -1,12 +1,15 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function - +import contextlib import copy +import enum +import functools import inspect +import itertools import linecache import sys -import warnings +import types +import typing from operator import itemgetter @@ -14,37 +17,23 @@ from operator import itemgetter # having the thread-local in the globals here. from . import _compat, _config, setters from ._compat import ( - HAS_F_STRINGS, - PY2, PY310, - PYPY, - isclass, - iteritems, - metadata_proxy, - new_class, - ordered_dict, - set_closure_cell, + PY_3_8_PLUS, + _AnnotationExtractor, + get_generic_base, ) from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, NotAnAttrsClassError, - PythonTooOldError, UnannotatedAttributeError, ) -if not PY2: - import typing - - # This is used at least twice, so cache it here. _obj_setattr = object.__setattr__ _init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) +_init_factory_pat = "__attr_factory_%s" _classvar_prefixes = ( "typing.ClassVar", "t.ClassVar", @@ -56,7 +45,7 @@ _classvar_prefixes = ( # (when slots=True) _hash_cache_field = "_attrs_cached_hash" -_empty_metadata_singleton = metadata_proxy({}) +_empty_metadata_singleton = types.MappingProxyType({}) # Unique object for unequivocal getattr() defaults. _sentinel = object() @@ -64,21 +53,18 @@ _sentinel = object() _ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) -class _Nothing(object): +class _Nothing(enum.Enum): """ - Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + Sentinel to indicate the lack of a value when ``None`` is ambiguous. - ``_Nothing`` is a singleton. There is only ever one of it. + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. """ - _singleton = None - - def __new__(cls): - if _Nothing._singleton is None: - _Nothing._singleton = super(_Nothing, cls).__new__(cls) - return _Nothing._singleton + NOTHING = enum.auto() def __repr__(self): return "NOTHING" @@ -86,11 +72,8 @@ class _Nothing(object): def __bool__(self): return False - def __len__(self): - return 0 # __bool__ for Python 2 - -NOTHING = _Nothing() +NOTHING = _Nothing.NOTHING """ Sentinel to indicate the lack of a value when ``None`` is ambiguous. """ @@ -108,17 +91,8 @@ class _CacheHashWrapper(int): See GH #613 for more details. """ - if PY2: - # For some reason `type(None)` isn't callable in Python 2, but we don't - # actually need a constructor for None objects, we just need any - # available function that returns None. - def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): - return _none_constructor, _args - - else: - - def __reduce__(self, _none_constructor=type(None), _args=()): - return _none_constructor, _args + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 + return _none_constructor, _args def attrib( @@ -136,16 +110,20 @@ def attrib( eq=None, order=None, on_setattr=None, + alias=None, ): """ Create a new attribute on a class. .. warning:: - Does *not* do anything unless the class is also decorated with - `attr.s`! + Does *not* do anything unless the class is also decorated with `attr.s` + / `attrs.define` / and so on! - :param default: A value that is used if an ``attrs``-generated ``__init__`` + Please consider using `attrs.field` in new code (``attr.ib`` will *never* + go away, though). + + :param default: A value that is used if an *attrs*-generated ``__init__`` is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. @@ -154,17 +132,17 @@ def attrib( or dicts). If a default is not set (or set manually to `attrs.NOTHING`), a value - *must* be supplied when instantiating; otherwise a `TypeError` - will be raised. + *must* be supplied when instantiating; otherwise a `TypeError` will be + raised. The default can also be set using decorator notation as shown below. - :type default: Any value + .. seealso:: `defaults` :param callable factory: Syntactic sugar for ``default=attr.Factory(factory)``. - :param validator: `callable` that is called by ``attrs``-generated + :param validator: `callable` that is called by *attrs*-generated ``__init__`` methods after the instance has been initialized. They receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. @@ -172,77 +150,90 @@ def attrib( The return value is *not* inspected so the validator has to throw an exception itself. - If a `list` is passed, its items are treated as validators and must - all pass. + If a `list` is passed, its items are treated as validators and must all + pass. Validators can be globally disabled and re-enabled using - `get_run_validators`. + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. The validator can also be set using decorator notation as shown below. + .. seealso:: :ref:`validators` + :type validator: `callable` or a `list` of `callable`\\ s. - :param repr: Include this attribute in the generated ``__repr__`` - method. If ``True``, include the attribute; if ``False``, omit it. By - default, the built-in ``repr()`` function is used. To override how the - attribute value is formatted, pass a ``callable`` that takes a single - value and returns a string. Note that the resulting string is used - as-is, i.e. it will be used directly *instead* of calling ``repr()`` - (the default). + :param repr: Include this attribute in the generated ``__repr__`` method. + If ``True``, include the attribute; if ``False``, omit it. By default, + the built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value and + returns a string. Note that the resulting string is used as-is, i.e. it + will be used directly *instead* of calling ``repr()`` (the default). :type repr: a `bool` or a `callable` to use a custom function. - :param eq: If ``True`` (default), include this attribute in the - generated ``__eq__`` and ``__ne__`` methods that check two instances - for equality. To override how the attribute value is compared, - pass a ``callable`` that takes a single value and returns the value - to be compared. + :param eq: If ``True`` (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + ``callable`` that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` :type eq: a `bool` or a `callable`. :param order: If ``True`` (default), include this attributes in the - generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. - To override how the attribute value is ordered, - pass a ``callable`` that takes a single value and returns the value - to be ordered. + generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a ``callable`` that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` :type order: a `bool` or a `callable`. :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` :type cmp: a `bool` or a `callable`. - :param Optional[bool] hash: Include this attribute in the generated + :param bool | None hash: Include this attribute in the generated ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This is the correct behavior according the Python spec. Setting this value to anything else than ``None`` is *discouraged*. + + .. seealso:: `hashing` :param bool init: Include this attribute in the generated ``__init__`` method. It is possible to set this to ``False`` and set a default value. In that case this attributed is unconditionally initialized with the specified default value or factory. - :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value - to the desired format. It is given the passed-in value, and the - returned value will be used as the new value of the attribute. The - value is converted before being passed to the validator, if any. - :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_). - This argument is provided for backward compatibility. - Regardless of the approach used, the type will be stored on - ``Attribute.type``. - - Please note that ``attrs`` doesn't do anything with this metadata by - itself. You can use it as part of your own code or for - `static type checking <types>`. - :param kw_only: Make this attribute keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). + + .. seealso:: `init` + :param callable converter: `callable` that is called by *attrs*-generated + ``__init__`` methods to convert attribute's value to the desired + format. It is given the passed-in value, and the returned value will + be used as the new value of the attribute. The value is converted + before being passed to the validator, if any. + + .. seealso:: :ref:`converters` + :param dict | None metadata: An arbitrary mapping, to be used by + third-party components. See `extending-metadata`. + + :param type: The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). This + argument is provided for backward compatibility. Regardless of the + approach used, the type will be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking <types>`. + :param bool kw_only: Make this attribute keyword-only in the generated + ``__init__`` (if ``init`` is ``False``, this parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. :type on_setattr: `callable`, or a list of callables, or `None`, or `attrs.setters.NO_OP` + :param str | None alias: Override this attribute's parameter name in the + generated ``__init__`` method. If left `None`, default to ``name`` + stripped of leading underscores. See `private-attributes`. .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -265,24 +256,25 @@ def attrib( .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True ) if hash is not None and hash is not True and hash is not False: - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) if factory is not None: if default is not NOTHING: - raise ValueError( - "The `default` and `factory` arguments are mutually " - "exclusive." + msg = ( + "The `default` and `factory` arguments are mutually exclusive." ) + raise ValueError(msg) if not callable(factory): - raise ValueError("The `factory` argument must be a callable.") + msg = "The `factory` argument must be a callable." + raise ValueError(msg) default = Factory(factory) if metadata is None: @@ -314,6 +306,7 @@ def attrib( order=order, order_key=order_key, on_setattr=on_setattr, + alias=alias, ) @@ -325,13 +318,11 @@ def _compile_and_eval(script, globs, locs=None, filename=""): eval(bytecode, globs, locs) -def _make_method(name, script, filename, globs=None): +def _make_method(name, script, filename, globs): """ Create the method with the script given and return the method object. """ locs = {} - if globs is None: - globs = {} # In order of debuggers like PDB being able to step through the code, # we add a fake linecache entry. @@ -347,9 +338,9 @@ def _make_method(name, script, filename, globs=None): old_val = linecache.cache.setdefault(filename, linecache_tuple) if old_val == linecache_tuple: break - else: - filename = "{}-{}>".format(base_filename[:-1], count) - count += 1 + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 _compile_and_eval(script, globs, locs, filename) @@ -366,15 +357,15 @@ def _make_attr_tuple_class(cls_name, attr_names): __slots__ = () x = property(itemgetter(0)) """ - attr_class_name = "{}Attributes".format(cls_name) + attr_class_name = f"{cls_name}Attributes" attr_class_template = [ - "class {}(tuple):".format(attr_class_name), + f"class {attr_class_name}(tuple):", " __slots__ = ()", ] if attr_names: for i, attr_name in enumerate(attr_names): attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) + f" {attr_name} = _attrs_property(_attrs_itemgetter({i}))" ) else: attr_class_template.append(" pass") @@ -418,8 +409,6 @@ def _is_class_var(annot): def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). - - Requires Python 3. """ attr = getattr(cls, attrib_name, _sentinel) if attr is _sentinel: @@ -443,13 +432,6 @@ def _get_annotations(cls): return {} -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - def _collect_base_attrs(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. @@ -463,7 +445,7 @@ def _collect_base_attrs(cls, taken_attr_names): if a.inherited or a.name in taken_attr_names: continue - a = a.evolve(inherited=True) + a = a.evolve(inherited=True) # noqa: PLW2901 base_attrs.append(a) base_attr_map[a.name] = base_cls @@ -501,7 +483,7 @@ def _collect_base_attrs_broken(cls, taken_attr_names): if a.name in taken_attr_names: continue - a = a.evolve(inherited=True) + a = a.evolve(inherited=True) # noqa: PLW2901 taken_attr_names.add(a.name) base_attrs.append(a) base_attr_map[a.name] = base_cls @@ -526,10 +508,7 @@ def _transform_attrs( anns = _get_annotations(cls) if these is not None: - ca_list = [(name, ca) for name, ca in iteritems(these)] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) + ca_list = list(these.items()) elif auto_attribs is True: ca_names = { name @@ -545,10 +524,7 @@ def _transform_attrs( a = cd.get(attr_name, NOTHING) if not isinstance(a, _CountingAttr): - if a is NOTHING: - a = attrib() - else: - a = attrib(default=a) + a = attrib() if a is NOTHING else attrib(default=a) ca_list.append((attr_name, a)) unannotated = ca_names - annot_names @@ -599,10 +575,8 @@ def _transform_attrs( had_default = False for a in (a for a in attrs if a.init is not False and a.kw_only is False): if had_default is True and a.default is NOTHING: - raise ValueError( - "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) - ) + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) if had_default is False and a.default is not NOTHING: had_default = True @@ -610,6 +584,14 @@ def _transform_attrs( if field_transformer is not None: attrs = field_transformer(cls, attrs) + # Resolve default field alias after executing field_transformer. + # This allows field_transformer to differentiate between explicit vs + # default aliases and supply their own defaults. + attrs = [ + a.evolve(alias=_default_init_alias_for(a.name)) if not a.alias else a + for a in attrs + ] + # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] @@ -618,28 +600,75 @@ def _transform_attrs( return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) -if PYPY: +def _make_cached_property_getattr( + cached_properties, + original_getattr, + cls, +): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper():", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " if hasattr(super(), '__getattr__'):", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper()", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _obj_setattr.__get__, + "_cls": cls, + "original_getattr": original_getattr, + } - raise FrozenInstanceError() + return _make_method( + "__getattr__", + "\n".join(lines), + unique_filename, + glob, + ) -else: - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError() def _frozen_delattrs(self, name): @@ -649,7 +678,7 @@ def _frozen_delattrs(self, name): raise FrozenInstanceError() -class _ClassBuilder(object): +class _ClassBuilder: """ Iteratively build *one* class. """ @@ -665,6 +694,7 @@ class _ClassBuilder(object): "_delete_attribs", "_frozen", "_has_pre_init", + "_pre_init_has_args", "_has_post_init", "_is_exc", "_on_setattr", @@ -703,7 +733,7 @@ class _ClassBuilder(object): self._cls = cls self._cls_dict = dict(cls.__dict__) if slots else {} self._attrs = attrs - self._base_names = set(a.name for a in base_attrs) + self._base_names = {a.name for a in base_attrs} self._base_attr_map = base_map self._attr_names = tuple(a.name for a in attrs) self._slots = slots @@ -711,6 +741,13 @@ class _ClassBuilder(object): self._weakref_slot = weakref_slot self._cache_hash = cache_hash self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) self._delete_attribs = not bool(these) self._is_exc = is_exc @@ -760,17 +797,35 @@ class _ClassBuilder(object): ) = self._make_getstate_setstate() def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + return f"<_ClassBuilder(cls={self._cls.__name__})>" - def build_class(self): - """ - Finalize class based on the accumulated configuration. + if PY310: + import abc + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + + return self.abc.update_abstractmethods( + self._patch_original_class() + ) + + else: + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: return self._patch_original_class() def _patch_original_class(self): @@ -787,13 +842,11 @@ class _ClassBuilder(object): name not in base_names and getattr(cls, name, _sentinel) is not _sentinel ): - try: + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): delattr(cls, name) - except AttributeError: - # This can happen if a base class defines a class - # variable and we want to set an attribute with the - # same name by using only a type annotation. - pass # Attach our dunder methods. for name, value in self._cls_dict.items(): @@ -807,7 +860,7 @@ class _ClassBuilder(object): cls.__attrs_own_setattr__ = False if not self._has_custom_setattr: - cls.__setattr__ = object.__setattr__ + cls.__setattr__ = _obj_setattr return cls @@ -817,8 +870,8 @@ class _ClassBuilder(object): """ cd = { k: v - for k, v in iteritems(self._cls_dict) - if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") + for k, v in self._cls_dict.items() + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") } # If our class doesn't have its own implementation of __setattr__ @@ -835,12 +888,12 @@ class _ClassBuilder(object): if not self._has_custom_setattr: for base_cls in self._cls.__bases__: if base_cls.__dict__.get("__attrs_own_setattr__", False): - cd["__setattr__"] = object.__setattr__ + cd["__setattr__"] = _obj_setattr break # Traverse the MRO to collect existing slots # and check for an existing __weakref__. - existing_slots = dict() + existing_slots = {} weakref_inherited = False for base_cls in self._cls.__mro__[1:-1]: if base_cls.__dict__.get("__weakref__", None) is not None: @@ -863,38 +916,76 @@ class _ClassBuilder(object): ): names += ("__weakref__",) + if PY_3_8_PLUS: + cached_properties = { + name: cached_property.func + for name, cached_property in cd.items() + if isinstance(cached_property, functools.cached_property) + } + else: + # `functools.cached_property` was introduced in 3.8. + # So can't be used before this. + cached_properties = {} + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + # Add cached properties to names for slotting. + names += tuple(cached_properties.keys()) + + for name in cached_properties: + # Clear out function from class to avoid clashing. + del cd[name] + + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + # We only add the names of attributes that aren't inherited. # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class # that are defined in parent classes. - # As their descriptors may be overriden by a child class, + # As their descriptors may be overridden by a child class, # we collect them here and update the class dict reused_slots = { slot: slot_descriptor - for slot, slot_descriptor in iteritems(existing_slots) + for slot, slot_descriptor in existing_slots.items() if slot in slot_names } slot_names = [name for name in slot_names if name not in reused_slots] cd.update(reused_slots) if self._cache_hash: slot_names.append(_hash_cache_field) + cd["__slots__"] = tuple(slot_names) - qualname = getattr(self._cls, "__qualname__", None) - if qualname is not None: - cd["__qualname__"] = qualname + cd["__qualname__"] = self._cls.__qualname__ # Create new class based on old class and our methods. cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) # The following is a fix for - # <https://github.com/python-attrs/attrs/issues/102>. On Python 3, - # if a method mentions `__class__` or uses the no-arg super(), the + # <https://github.com/python-attrs/attrs/issues/102>. + # If a method mentions `__class__` or uses the no-arg super(), the # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a # clone, we rewrite these references so it keeps working. - for item in cls.__dict__.values(): + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): if isinstance(item, (classmethod, staticmethod)): # Class- and staticmethods hide their functions inside. # These might need to be rewritten as well. @@ -911,12 +1002,12 @@ class _ClassBuilder(object): for cell in closure_cells: try: match = cell.cell_contents is self._cls - except ValueError: # ValueError: Cell is empty + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty pass else: if match: - set_closure_cell(cell, cls) - + cell.cell_contents = cls return cls def add_repr(self, ns): @@ -928,9 +1019,8 @@ class _ClassBuilder(object): def add_str(self): repr = self._cls_dict.get("__repr__") if repr is None: - raise ValueError( - "__str__ can only be generated if a __repr__ exists." - ) + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) def __str__(self): return self.__repr__() @@ -951,7 +1041,7 @@ class _ClassBuilder(object): """ Automatically created by attrs. """ - return tuple(getattr(self, name) for name in state_attr_names) + return {name: getattr(self, name) for name in state_attr_names} hash_caching_enabled = self._cache_hash @@ -959,9 +1049,16 @@ class _ClassBuilder(object): """ Automatically created by attrs. """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) + __bound_setattr = _obj_setattr.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to @@ -994,6 +1091,7 @@ class _ClassBuilder(object): self._cls, self._attrs, self._has_pre_init, + self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, @@ -1020,6 +1118,7 @@ class _ClassBuilder(object): self._cls, self._attrs, self._has_pre_init, + self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, @@ -1068,9 +1167,8 @@ class _ClassBuilder(object): if self._has_custom_setattr: # We need to write a __setattr__ but there already is one! - raise ValueError( - "Can't combine custom __setattr__ with on_setattr hooks." - ) + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) # docstring comes from _add_method_dunders def __setattr__(self, name, val): @@ -1093,41 +1191,29 @@ class _ClassBuilder(object): """ Add __module__ and __qualname__ to a *method* if possible. """ - try: + with contextlib.suppress(AttributeError): method.__module__ = self._cls.__module__ - except AttributeError: - pass - try: - method.__qualname__ = ".".join( - (self._cls.__qualname__, method.__name__) - ) - except AttributeError: - pass + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" - try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, + with contextlib.suppress(AttributeError): + method.__doc__ = ( + "Method generated by attrs for class " + f"{self._cls.__qualname__}." ) - except AttributeError: - pass return method -_CMP_DEPRECATION = ( - "The usage of `cmp` is deprecated and will be removed on or after " - "2021-06-01. Please use `eq` and `order` instead." -) - - def _determine_attrs_eq_order(cmp, eq, order, default_eq): """ Validate the combination of *cmp*, *eq*, and *order*. Derive the effective values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) # cmp takes precedence due to bw-compatibility. if cmp is not None: @@ -1142,7 +1228,8 @@ def _determine_attrs_eq_order(cmp, eq, order, default_eq): order = eq if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) return eq, order @@ -1153,7 +1240,8 @@ def _determine_attrib_eq_order(cmp, eq, order, default_eq): values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) def decide_callable_or_boolean(value): """ @@ -1183,7 +1271,8 @@ def _determine_attrib_eq_order(cmp, eq, order, default_eq): order, order_key = decide_callable_or_boolean(order) if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) return eq, eq_key, order, order_key @@ -1199,8 +1288,6 @@ def _determine_whether_to_implement( whose presence signal that the user has implemented it themselves. Return *default* if no reason for either for or against is found. - - auto_detect must be False on Python 2. """ if flag is True or flag is False: return flag @@ -1240,24 +1327,24 @@ def attrs( on_setattr=None, field_transformer=None, match_args=True, + unsafe_hash=None, ): r""" - A class decorator that adds `dunder - <https://wiki.python.org/moin/DunderAlias>`_\ -methods according to the + A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. - :param these: A dictionary of name to `attr.ib` mappings. This is - useful to avoid the definition of your attributes within the class body + Please consider using `attrs.define` / `attrs.frozen` in new code + (``attr.s`` will *never* go away, though). + + :param these: A dictionary of name to `attr.ib` mappings. This is useful + to avoid the definition of your attributes within the class body because you can't (e.g. if you want to add ``__repr__`` methods to Django models) or don't want to. - If *these* is not ``None``, ``attrs`` will *not* search the class body + If *these* is not ``None``, *attrs* will *not* search the class body for attributes and will *not* remove any attributes from it. - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. + The order is deduced from the order of the attributes inside *these*. :type these: `dict` of `str` to `attr.ib` @@ -1270,79 +1357,89 @@ def attrs( arguments is implemented in the *current* class (i.e. it is *not* inherited from some base class). - So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* - ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible - ``__ne__`` by default, so it *should* be enough to only implement - ``__eq__`` in most cases). + So for example by implementing ``__eq__`` on a class yourself, *attrs* + will deduce ``eq=False`` and will create *neither* ``__eq__`` *nor* + ``__ne__`` (but Python classes come with a sensible ``__ne__`` by + default, so it *should* be enough to only implement ``__eq__`` in most + cases). .. warning:: - If you prevent ``attrs`` from creating the ordering methods for you + If you prevent *attrs* from creating the ordering methods for you (``order=False``, e.g. by implementing ``__le__``), it becomes *your* responsibility to make sure its ordering is sound. The best way is to use the `functools.total_ordering` decorator. - Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, - *cmp*, or *hash* overrides whatever *auto_detect* would determine. - - *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - an `attrs.exceptions.PythonTooOldError`. + Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, *cmp*, + or *hash* overrides whatever *auto_detect* would determine. :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. + representation of *attrs* attributes.. :param bool str: Create a ``__str__`` method that is identical to - ``__repr__``. This is usually not necessary except for - `Exception`\ s. - :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + ``__repr__``. This is usually not necessary except for `Exception`\ s. + :param bool | None eq: If ``True`` or ``None`` (default), add ``__eq__`` and ``__ne__`` methods that check two instances for equality. - They compare the instances as if they were tuples of their ``attrs`` + They compare the instances as if they were tuples of their *attrs* attributes if and only if the types of both classes are *identical*! - :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + + .. seealso:: `comparison` + :param bool | None order: If ``True``, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` methods that behave like *eq* above and allow instances to be ordered. If ``None`` (default) mirror value of *eq*. - :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* - and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method - is generated according how *eq* and *frozen* are set. - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + .. seealso:: `comparison` + :param bool | None cmp: Setting *cmp* is equivalent to setting *eq* and + *order* to the same value. Must not be mixed with *eq* or *order*. + + .. seealso:: `comparison` + :param bool | None unsafe_hash: If ``None`` (default), the ``__hash__`` + method is generated according how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for you. 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to None, marking it unhashable (which it is). 3. If *eq* is False, ``__hash__`` will be left untouched meaning the ``__hash__`` method of the base class will be used (if base class is ``object``, this means it will fall back to id-based hashing.). - Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you - didn't freeze it programmatically) by passing ``True`` or not. Both of - these cases are rather special and should be used carefully. - - See our documentation on `hashing`, Python's documentation on - `object.__hash__`, and the `GitHub issue that led to the default \ - behavior <https://github.com/python-attrs/attrs/issues/136>`_ for more - details. - :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument - name. If a ``__attrs_pre_init__`` method exists on the class, it will - be called before the class is initialized. If a ``__attrs_post_init__`` - method exists on the class, it will be called after the class is fully + Although not recommended, you can decide for yourself and force *attrs* + to create one (e.g. if the class is immutable even though you didn't + freeze it programmatically) by passing ``True`` or not. Both of these + cases are rather special and should be used carefully. + + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ + behavior <https://github.com/python-attrs/attrs/issues/136>`_ for + more details. + + :param bool | None hash: Alias for *unsafe_hash*. *unsafe_hash* takes + precedence. + :param bool init: Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name. If + a ``__attrs_pre_init__`` method exists on the class, it will be called + before the class is initialized. If a ``__attrs_post_init__`` method + exists on the class, it will be called after the class is fully initialized. - If ``init`` is ``False``, an ``__attrs_init__`` method will be - injected instead. This allows you to define a custom ``__init__`` - method that can do pre-init work such as ``super().__init__()``, - and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class <slotted classes>` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry <slotted classes>`. + If ``init`` is ``False``, an ``__attrs_init__`` method will be injected + instead. This allows you to define a custom ``__init__`` method that + can do pre-init work such as ``super().__init__()``, and then call + ``__attrs_init__()`` and ``__attrs_post_init__()``. + + .. seealso:: `init` + :param bool slots: Create a :term:`slotted class <slotted classes>` that's + more memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, so + we encourage you to read the :term:`glossary entry <slotted classes>`. :param bool frozen: Make instances immutable after initialization. If someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. + `attrs.exceptions.FrozenInstanceError` is raised. .. note:: @@ -1357,21 +1454,21 @@ def attrs( 4. If a class is frozen, you cannot modify ``self`` in ``__attrs_post_init__`` or a self-written ``__init__``. You can - circumvent that limitation by using - ``object.__setattr__(self, "attribute_name", value)``. + circumvent that limitation by using ``object.__setattr__(self, + "attribute_name", value)``. 5. Subclasses of a frozen class are frozen too. :param bool weakref_slot: Make instances weak-referenceable. This has no effect unless ``slots`` is also enabled. - :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated - attributes (Python 3.6 and later only) from the class body. + :param bool auto_attribs: If ``True``, collect :pep:`526`-annotated + attributes from the class body. - In this case, you **must** annotate every field. If ``attrs`` - encounters a field that is set to an `attr.ib` but lacks a type - annotation, an `attr.exceptions.UnannotatedAttributeError` is - raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't - want to set a type. + In this case, you **must** annotate every field. If *attrs* encounters + a field that is set to an `attr.ib` but lacks a type annotation, an + `attr.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = attr.ib(...)`` if you don't want to set a + type. If you assign a value to those attributes (e.g. ``x: int = 42``), that value becomes the default value like if it were passed using @@ -1383,58 +1480,55 @@ def attrs( .. warning:: For features that use the attribute name to create decorators (e.g. - `validators <validators>`), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. + :ref:`validators <validators>`), you still *must* assign `attr.ib` + to them. Otherwise Python will either not find the name or try to + use the default value to call e.g. ``validator`` on it. These errors can be quite confusing and probably the most common bug report on our bug tracker. - .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ - :param bool kw_only: Make all attributes keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param bool cache_hash: Ensure that the object's hash code is computed - only once and stored on the object. If this is set to ``True``, - hashing must be either explicitly or implicitly enabled for this - class. If the hash code is cached, avoid any reassignments of - fields involved in hash code computation or mutations of the objects - those fields point to after object creation. If such changes occur, - the behavior of the object's hash code is undefined. - :param bool auto_exc: If the class subclasses `BaseException` - (which implicitly includes any subclass of any exception), the - following happens to behave like a well-behaved Python exceptions - class: + :param bool kw_only: Make all attributes keyword-only in the generated + ``__init__`` (if ``init`` is ``False``, this parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed only + once and stored on the object. If this is set to ``True``, hashing + must be either explicitly or implicitly enabled for this class. If the + hash code is cached, avoid any reassignments of fields involved in hash + code computation or mutations of the objects those fields point to + after object creation. If such changes occur, the behavior of the + object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` (which + implicitly includes any subclass of any exception), the following + happens to behave like a well-behaved Python exceptions class: - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will + instances compare and hash by the instance's ids (N.B. *attrs* will *not* remove existing implementations of ``__hash__`` or the equality methods. It just won't add own ones.), - all attributes that are either passed into ``__init__`` or have a default value are additionally available as a tuple in the ``args`` attribute, - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + :param bool collect_by_mro: Setting this to `True` fixes the way *attrs* collects attributes from base classes. The default behavior is incorrect in certain cases of multiple inheritance. It should be on by default but is kept off for backward-compatibility. - See issue `#428 <https://github.com/python-attrs/attrs/issues/428>`_ for - more details. + .. seealso:: + Issue `#428 <https://github.com/python-attrs/attrs/issues/428>`_ - :param Optional[bool] getstate_setstate: + :param bool | None getstate_setstate: .. note:: This is usually only interesting for slotted classes and you should probably just set *auto_detect* to `True`. - If `True`, ``__getstate__`` and - ``__setstate__`` are generated and attached to the class. This is - necessary for slotted classes to be pickleable. If left `None`, it's - `True` by default for slotted classes and ``False`` for dict classes. + If `True`, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left `None`, it's `True` by default for slotted classes + and ``False`` for dict classes. - If *auto_detect* is `True`, and *getstate_setstate* is left `None`, - and **either** ``__getstate__`` or ``__setstate__`` is detected directly - on the class (i.e. not inherited), it is set to `False` (this is usually + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, and + **either** ``__getstate__`` or ``__setstate__`` is detected directly on + the class (i.e. not inherited), it is set to `False` (this is usually what you want). :param on_setattr: A callable that is run whenever the user attempts to set @@ -1448,19 +1542,22 @@ def attrs( If a list of callables is passed, they're automatically wrapped in an `attrs.setters.pipe`. + :type on_setattr: `callable`, or a list of callables, or `None`, or + `attrs.setters.NO_OP` - :param Optional[callable] field_transformer: - A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use - this, e.g., to automatically add converters or validators to - fields based on their types. See `transform-fields` for more details. + :param callable | None field_transformer: + A function that is called with the original class object and all fields + right before *attrs* finalizes the class. You can use this, e.g., to + automatically add converters or validators to fields based on their + types. + + .. seealso:: `transform-fields` :param bool match_args: If `True` (default), set ``__match_args__`` on the class to support - `PEP 634 <https://www.python.org/dev/peps/pep-0634/>`_ (Structural - Pattern Matching). It is a tuple of all positional-only ``__init__`` - parameter names on Python 3.10 and later. Ignored on older Python - versions. + :pep:`634` (Structural Pattern Matching). It is a tuple of all + non-keyword-only ``__init__`` parameter names on Python 3.10 and later. + Ignored on older Python versions. .. versionadded:: 16.0.0 *slots* .. versionadded:: 16.1.0 *frozen* @@ -1496,23 +1593,19 @@ def attrs( .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ - if auto_detect and PY2: - raise PythonTooOldError( - "auto_detect only works on Python 3 and later." - ) - eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) - hash_ = hash # work around the lack of nonlocal + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash if isinstance(on_setattr, (list, tuple)): on_setattr = setters.pipe(*on_setattr) def wrap(cls): - - if getattr(cls, "__class__", None) is None: - raise TypeError("attrs only works with new-style classes.") - is_frozen = frozen or _has_frozen_base_class(cls) is_exc = auto_exc is True and issubclass(cls, BaseException) has_own_setattr = auto_detect and _has_own_attribute( @@ -1520,7 +1613,8 @@ def attrs( ) if has_own_setattr and is_frozen: - raise ValueError("Can't freeze a class with a custom __setattr__.") + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) builder = _ClassBuilder( cls, @@ -1563,28 +1657,25 @@ def attrs( builder.add_setattr() + nonlocal hash if ( - hash_ is None + hash is None and auto_detect is True and _has_own_attribute(cls, "__hash__") ): hash = False - else: - hash = hash_ + if hash is not True and hash is not False and hash is not None: # Can't use `hash in` because 1 == True for example. - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - elif hash is False or (hash is None and eq is False) or is_exc: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if hash is False or (hash is None and eq is False) or is_exc: # Don't do anything. Should fall back to __object__'s __hash__ # which is by id. if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) elif hash is True or ( hash is None and eq is True and is_frozen is True ): @@ -1593,11 +1684,8 @@ def attrs( else: # Raise TypeError on attempts to hash. if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) builder.make_unhashable() if _determine_whether_to_implement( @@ -1607,10 +1695,8 @@ def attrs( else: builder.add_attrs_init() if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) if ( PY310 @@ -1625,8 +1711,8 @@ def attrs( # if it's used as `@attrs` but ``None`` if used as `@attrs()`. if maybe_cls is None: return wrap - else: - return wrap(maybe_cls) + + return wrap(maybe_cls) _attrs = attrs @@ -1636,39 +1722,22 @@ Internal alias so we can use it in functions that take an argument called """ -if PY2: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return ( - getattr(cls.__setattr__, "__module__", None) - == _frozen_setattrs.__module__ - and cls.__setattr__.__name__ == _frozen_setattrs.__name__ - ) - -else: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return cls.__setattr__ == _frozen_setattrs +def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ is _frozen_setattrs def _generate_unique_filename(cls, func_name): """ Create a "filename" suitable for a function being generated. """ - unique_filename = "<attrs generated {0} {1}.{2}>".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), + return ( + f"<attrs generated {func_name} {cls.__module__}." + f"{getattr(cls, '__qualname__', cls.__name__)}>" ) - return unique_filename def _make_hash(cls, attrs, frozen, cache_hash): @@ -1680,6 +1749,8 @@ def _make_hash(cls, attrs, frozen, cache_hash): unique_filename = _generate_unique_filename(cls, "hash") type_hash = hash(unique_filename) + # If eq is custom generated, we need to include the functions in globs + globs = {} hash_def = "def __hash__(self" hash_func = "hash((" @@ -1687,13 +1758,9 @@ def _make_hash(cls, attrs, frozen, cache_hash): if not cache_hash: hash_def += "):" else: - if not PY2: - hash_def += ", *" + hash_def += ", *" - hash_def += ( - ", _cache_wrapper=" - + "__import__('attr._make')._make._CacheHashWrapper):" - ) + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" hash_func = "_cache_wrapper(" + hash_func closing_braces += ")" @@ -1709,32 +1776,39 @@ def _make_hash(cls, attrs, frozen, cache_hash): method_lines.extend( [ indent + prefix + hash_func, - indent + " %d," % (type_hash,), + indent + f" {type_hash},", ] ) for a in attrs: - method_lines.append(indent + " self.%s," % a.name) + if a.eq_key: + cmp_name = f"_{a.name}_key" + globs[cmp_name] = a.eq_key + method_lines.append( + indent + f" {cmp_name}(self.{a.name})," + ) + else: + method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + method_lines.append(tab + f"if self.{_hash_cache_field} is None:") if frozen: append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + f"object.__setattr__(self, '{_hash_cache_field}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 + f"self.{_hash_cache_field} = ", tab * 2 ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) + method_lines.append(tab + f"return self.{_hash_cache_field}") else: append_hash_computation_lines("return ", tab) script = "\n".join(method_lines) - return _make_method("__hash__", script, unique_filename) + return _make_method("__hash__", script, unique_filename, globs) def _add_hash(cls, attrs): @@ -1785,29 +1859,17 @@ def _make_eq(cls, attrs): others = [" ) == ("] for a in attrs: if a.eq_key: - cmp_name = "_%s_key" % (a.name,) + cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) + lines.append(f" {cmp_name}(self.{a.name}),") + others.append(f" {cmp_name}(other.{a.name}),") else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) + lines.append(f" self.{a.name},") + others.append(f" other.{a.name},") - lines += others + [" )"] + lines += [*others, " )"] else: lines.append(" return True") @@ -1885,134 +1947,61 @@ def _add_eq(cls, attrs=None): return cls -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False +def _make_repr(attrs, ns, cls): + unique_filename = _generate_unique_filename(cls, "repr") + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ - - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - qualname = getattr(real_cls, "__qualname__", None) - if qualname is not None: # pragma: no cover - # This case only happens on Python 3.5 and 3.6. We exclude - # it from coverage, because we don't want to slow down our - # test suite by running them under coverage too for this - # one line. - class_name = qualname.rsplit(">.", 1)[-1] - else: - class_name = real_cls.__name__ - else: - class_name = ns + "." + real_cls.__name__ + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] - return __repr__ + return _make_method( + "__repr__", "\n".join(lines), unique_filename, globs=globs + ) def _add_repr(cls, ns=None, attrs=None): @@ -2028,7 +2017,7 @@ def _add_repr(cls, ns=None, attrs=None): def fields(cls): """ - Return the tuple of ``attrs`` attributes for a class. + Return the tuple of *attrs* attributes for a class. The tuple also allows accessing the fields by their names (see below for examples). @@ -2036,50 +2025,61 @@ def fields(cls): :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. :rtype: tuple (with name accessors) of `attrs.Attribute` - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return attrs def fields_dict(cls): """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose + Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. + :rtype: dict .. versionadded:: 18.1.0 """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") + if not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict(((a.name, a) for a in attrs)) + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} def validate(inst): @@ -2088,7 +2088,7 @@ def validate(inst): Leaves all exceptions through. - :param inst: Instance of a class with ``attrs`` attributes. + :param inst: Instance of a class with *attrs* attributes. """ if _config._run_validators is False: return @@ -2114,6 +2114,7 @@ def _make_init( cls, attrs, pre_init, + pre_init_has_args, post_init, frozen, slots, @@ -2128,7 +2129,8 @@ def _make_init( ) if frozen and has_cls_on_setattr: - raise ValueError("Frozen classes can't use on_setattr.") + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) needs_cached_setattr = cache_hash or frozen filtered_attrs = [] @@ -2142,7 +2144,8 @@ def _make_init( if a.on_setattr is not None: if frozen is True: - raise ValueError("Frozen classes can't use on_setattr.") + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) needs_cached_setattr = True elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: @@ -2155,6 +2158,7 @@ def _make_init( frozen, slots, pre_init, + pre_init_has_args, post_init, cache_hash, base_attr_map, @@ -2172,7 +2176,7 @@ def _make_init( if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. - globs["_cached_setattr"] = _obj_setattr + globs["_cached_setattr_get"] = _obj_setattr.__get__ init = _make_method( "__attrs_init__" if attrs_init else "__init__", @@ -2189,7 +2193,7 @@ def _setattr(attr_name, value_var, has_on_setattr): """ Use the cached object.setattr to set *attr_name* to *value_var*. """ - return "_setattr('%s', %s)" % (attr_name, value_var) + return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter(attr_name, value_var, has_on_setattr): @@ -2212,7 +2216,7 @@ def _assign(attr_name, value, has_on_setattr): if has_on_setattr: return _setattr(attr_name, value, True) - return "self.%s = %s" % (attr_name, value) + return f"self.{attr_name} = {value}" def _assign_with_converter(attr_name, value_var, has_on_setattr): @@ -2230,68 +2234,12 @@ def _assign_with_converter(attr_name, value_var, has_on_setattr): ) -if PY2: - - def _unpack_kw_only_py2(attr_name, default=None): - """ - Unpack *attr_name* from _kw_only dict. - """ - if default is not None: - arg_default = ", %s" % default - else: - arg_default = "" - return "%s = _kw_only.pop('%s'%s)" % ( - attr_name, - attr_name, - arg_default, - ) - - def _unpack_kw_only_lines_py2(kw_only_args): - """ - Unpack all *kw_only_args* from _kw_only dict and handle errors. - - Given a list of strings "{attr_name}" and "{attr_name}={default}" - generates list of lines of code that pop attrs from _kw_only dict and - raise TypeError similar to builtin if required attr is missing or - extra key is passed. - - >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) - try: - a = _kw_only.pop('a') - b = _kw_only.pop('b', 42) - except KeyError as _key_error: - raise TypeError( - ... - if _kw_only: - raise TypeError( - ... - """ - lines = ["try:"] - lines.extend( - " " + _unpack_kw_only_py2(*arg.split("=")) - for arg in kw_only_args - ) - lines += """\ -except KeyError as _key_error: - raise TypeError( - '__init__() missing required keyword-only argument: %s' % _key_error - ) -if _kw_only: - raise TypeError( - '__init__() got an unexpected keyword argument %r' - % next(iter(_kw_only)) - ) -""".split( - "\n" - ) - return lines - - def _attrs_to_init_script( attrs, frozen, slots, pre_init, + pre_init_has_args, post_init, cache_hash, base_attr_map, @@ -2317,7 +2265,7 @@ def _attrs_to_init_script( # Circumvent the __setattr__ descriptor to save one lookup per # assignment. # Note _setattr will be used again below if cache_hash is True - "_setattr = _cached_setattr.__get__(self, self.__class__)" + "_setattr = _cached_setattr_get(self)" ) if frozen is True: @@ -2335,7 +2283,7 @@ def _attrs_to_init_script( if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) - return "_inst_dict['%s'] = %s" % (attr_name, value_var) + return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name, value_var, has_on_setattr @@ -2373,22 +2321,21 @@ def _attrs_to_init_script( has_on_setattr = a.on_setattr is not None or ( a.on_setattr is not setters.NO_OP and has_cls_on_setattr ) - arg_name = a.name.lstrip("_") + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias has_factory = isinstance(a.default, Factory) - if has_factory and a.default.takes_self: - maybe_self = "self" - else: - maybe_self = "" + maybe_self = "self" if has_factory and a.default.takes_self else "" if a.init is False: if has_factory: - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( fmt_setter_with_converter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) @@ -2398,32 +2345,31 @@ def _attrs_to_init_script( lines.append( fmt_setter( attr_name, - init_factory_name + "(%s)" % (maybe_self,), + init_factory_name + f"({maybe_self})", has_on_setattr, ) ) names_for_globals[init_factory_name] = a.default.factory - else: - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) + elif a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, ) + ) elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: @@ -2442,14 +2388,14 @@ def _attrs_to_init_script( lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: - arg = "%s=NOTHING" % (arg_name,) + arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) + lines.append(f"if {arg_name} is not NOTHING:") - init_factory_name = _init_factory_pat.format(a.name) + init_factory_name = _init_factory_pat % (a.name,) if a.converter is not None: lines.append( " " @@ -2504,21 +2450,11 @@ def _attrs_to_init_script( if a.init is True: if a.type is not None and a.converter is None: annotations[arg_name] = a.type - elif a.converter is not None and not PY2: + elif a.converter is not None: # Try to get the type from the converter. - sig = None - try: - sig = inspect.signature(a.converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - sig_params = list(sig.parameters.values()) - if ( - sig_params - and sig_params[0].annotation - is not inspect.Parameter.empty - ): - annotations[arg_name] = sig_params[0].annotation + t = _AnnotationExtractor(a.converter).get_first_param_type() + if t: + annotations[arg_name] = t if attrs_to_validate: # we can skip this if there are no validators. names_for_globals["_config"] = _config @@ -2526,23 +2462,21 @@ def _attrs_to_init_script( for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a if post_init: lines.append("self.__attrs_post_init__()") - # because this is set only after __attrs_post_init is called, a crash + # because this is set only after __attrs_post_init__ is called, a crash # will result if post-init tries to access the hash code. This seemed # preferable to setting this beforehand, in which case alteration to # field values during post-init combined with post-init accessing the # hash code would result in silent bugs. if cache_hash: if frozen: - if slots: + if slots: # noqa: SIM108 # if frozen and slots, then _setattr defined above init_hash_cache = "_setattr('%s', %s)" else: @@ -2555,44 +2489,67 @@ def _attrs_to_init_script( # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) - lines.append("BaseException.__init__(self, %s)" % (vals,)) + lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) + pre_init_args = args if kw_only_args: - if PY2: - lines = _unpack_kw_only_lines_py2(kw_only_args) + lines + args += "%s*, %s" % ( + ", " if args else "", # leading comma + ", ".join(kw_only_args), # kw_only args + ) + pre_init_kw_only_args = ", ".join( + ["%s=%s" % (kw_arg, kw_arg) for kw_arg in kw_only_args] + ) + pre_init_args += ( + ", " if pre_init_args else "" + ) # handle only kwargs and no regular args + pre_init_args += pre_init_kw_only_args + + if pre_init and pre_init_has_args: + # If pre init method has arguments, pass same arguments as `__init__` + lines[0] = "self.__attrs_pre_init__(%s)" % pre_init_args - args += "%s**_kw_only" % (", " if args else "",) # leading comma - else: - args += "%s*, %s" % ( - ", " if args else "", # leading comma - ", ".join(kw_only_args), # kw_only args - ) return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", + "def %s(self, %s):\n %s\n" + % ( + ("__attrs_init__" if attrs_init else "__init__"), + args, + "\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, ) -class Attribute(object): +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + +class Attribute: """ *Read-only* representation of an attribute. + .. warning:: + + You should never instantiate this class yourself. + The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables @@ -2608,12 +2565,16 @@ class Attribute(object): - Validators get them passed as the first argument. - The :ref:`field transformer <transform-fields>` hook receives a list of them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.2.0 *inherited* is not taken into account for equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* For the full version history of the fields, see `attr.ib`. """ @@ -2635,6 +2596,7 @@ class Attribute(object): "kw_only", "inherited", "on_setattr", + "alias", ) def __init__( @@ -2656,13 +2618,14 @@ class Attribute(object): order=None, order_key=None, on_setattr=None, + alias=None, ): eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq_key or eq, order_key or order, True ) # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. @@ -2680,7 +2643,7 @@ class Attribute(object): bound_setattr( "metadata", ( - metadata_proxy(metadata) + types.MappingProxyType(dict(metadata)) # Shallow copy if metadata else _empty_metadata_singleton ), @@ -2689,6 +2652,7 @@ class Attribute(object): bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) def __setattr__(self, name, value): raise FrozenInstanceError() @@ -2699,9 +2663,8 @@ class Attribute(object): if type is None: type = ca.type elif ca.type is not None: - raise ValueError( - "Type annotation and type argument cannot both be present" - ) + msg = "Type annotation and type argument cannot both be present" + raise ValueError(msg) inst_dict = { k: getattr(ca, k) for k in Attribute.__slots__ @@ -2721,25 +2684,16 @@ class Attribute(object): type=type, cmp=None, inherited=False, - **inst_dict + **inst_dict, ) - @property - def cmp(self): - """ - Simulate the presence of a cmp attribute and warn. - """ - warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) - - return self.eq and self.order - - # Don't use attr.evolve since fields(Attribute) doesn't work + # Don't use attrs.evolve since fields(Attribute) doesn't work def evolve(self, **changes): """ Copy *self* and apply *changes*. - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. + This works similarly to `attrs.evolve` but that function does not work + with `Attribute`. It is mainly meant to be used for `transform-fields`. @@ -2768,14 +2722,14 @@ class Attribute(object): self._setattrs(zip(self.__slots__, state)) def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) + bound_setattr = _obj_setattr.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) else: bound_setattr( name, - metadata_proxy(value) + types.MappingProxyType(dict(value)) if value else _empty_metadata_singleton, ) @@ -2793,6 +2747,7 @@ _a = [ hash=(name != "metadata"), init=True, inherited=False, + alias=_default_init_alias_for(name), ) for name in Attribute.__slots__ ] @@ -2806,7 +2761,7 @@ Attribute = _add_hash( ) -class _CountingAttr(object): +class _CountingAttr: """ Intermediate representation of attributes that uses a counter to preserve the order in which the attributes have been defined. @@ -2831,37 +2786,42 @@ class _CountingAttr(object): "type", "kw_only", "on_setattr", + "alias", ) - __attrs_attrs__ = tuple( - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=True, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ) - for name in ( - "counter", - "_default", - "repr", - "eq", - "order", - "hash", - "init", - "on_setattr", - ) - ) + ( + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), Attribute( name="metadata", + alias="metadata", default=None, validator=None, repr=True, @@ -2896,6 +2856,7 @@ class _CountingAttr(object): order, order_key, on_setattr, + alias, ): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter @@ -2913,6 +2874,7 @@ class _CountingAttr(object): self.type = type self.kw_only = kw_only self.on_setattr = on_setattr + self.alias = alias def validator(self, meth): """ @@ -2949,7 +2911,7 @@ class _CountingAttr(object): _CountingAttr = _add_eq(_add_repr(_CountingAttr)) -class Factory(object): +class Factory: """ Stores a factory callable. @@ -2967,10 +2929,6 @@ class Factory(object): __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ self.factory = factory self.takes_self = takes_self @@ -3007,23 +2965,26 @@ _f = [ Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) -def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" A quick way to create a new class called *name* with *attrs*. :param str name: The name for the new class. :param attrs: A list of names or a dictionary of mappings of names to - attributes. + `attr.ib`\ s / `attrs.field`\ s. - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. + The order is deduced from the order of the names or attributes inside + *attrs*. Otherwise the order of the definition of the attributes is + used. :type attrs: `list` or `dict` :param tuple bases: Classes that the new class will subclass. + :param dict class_body: An optional dictionary of class attributes for the new class. + :param attributes_arguments: Passed unmodified to `attr.s`. :return: A new class with *attrs*. @@ -3031,19 +2992,23 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): .. versionadded:: 17.1.0 *bases* .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* """ if isinstance(attrs, dict): cls_dict = attrs elif isinstance(attrs, (list, tuple)): - cls_dict = dict((a, attrib()) for a in attrs) + cls_dict = {a: attrib() for a in attrs} else: - raise TypeError("attrs argument must be a dict or a list.") + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) pre_init = cls_dict.pop("__attrs_pre_init__", None) post_init = cls_dict.pop("__attrs_post_init__", None) user_init = cls_dict.pop("__init__", None) body = {} + if class_body is not None: + body.update(class_body) if pre_init is not None: body["__attrs_pre_init__"] = pre_init if post_init is not None: @@ -3051,18 +3016,16 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): if user_init is not None: body["__init__"] = user_init - type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) + type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) # For pickling to work, the __module__ variable needs to be set to the # frame where the class is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not # defined for arguments greater than 0 (IronPython). - try: + with contextlib.suppress(AttributeError, ValueError): type_.__module__ = sys._getframe(1).f_globals.get( "__name__", "__main__" ) - except (AttributeError, ValueError): - pass # We do it here for proper warnings with meaningful stacklevel. cmp = attributes_arguments.pop("cmp", None) @@ -3084,7 +3047,7 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): @attrs(slots=True, hash=True) -class _AndValidator(object): +class _AndValidator: """ Compose many validators to a single one. """ @@ -3138,36 +3101,19 @@ def pipe(*converters): return val - if not PY2: - if not converters: - # If the converter list is empty, pipe_converter is the identity. - A = typing.TypeVar("A") - pipe_converter.__annotations__ = {"val": A, "return": A} - else: - # Get parameter type. - sig = None - try: - sig = inspect.signature(converters[0]) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if ( - params - and params[0].annotation is not inspect.Parameter.empty - ): - pipe_converter.__annotations__["val"] = params[ - 0 - ].annotation - # Get return type. - sig = None - try: - sig = inspect.signature(converters[-1]) - except (ValueError, TypeError): # inspect failed - pass - if sig and sig.return_annotation is not inspect.Signature().empty: - pipe_converter.__annotations__[ - "return" - ] = sig.return_annotation + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = typing.TypeVar("A") + pipe_converter.__annotations__ = {"val": A, "return": A} + else: + # Get parameter type from first converter. + t = _AnnotationExtractor(converters[0]).get_first_param_type() + if t: + pipe_converter.__annotations__["val"] = t + + # Get return type from last converter. + rt = _AnnotationExtractor(converters[-1]).get_return_type() + if rt: + pipe_converter.__annotations__["return"] = rt return pipe_converter diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_next_gen.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_next_gen.py index 068253688ca..1fb9f259b53 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_next_gen.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_next_gen.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: MIT """ -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. """ @@ -26,6 +26,7 @@ def define( *, these=None, repr=None, + unsafe_hash=None, hash=None, init=None, slots=True, @@ -45,20 +46,24 @@ def define( match_args=True, ): r""" - Define an ``attrs`` class. + Define an *attrs* class. Differences to the classic `attr.s` that it uses underneath: - - Automatically detect whether or not *auto_attribs* should be `True` - (c.f. *auto_attribs* parameter). - - If *frozen* is `False`, run converters and validators when setting an - attribute by default. - - *slots=True* (see :term:`slotted classes` for potentially surprising - behaviors) + - Automatically detect whether or not *auto_attribs* should be `True` (c.f. + *auto_attribs* parameter). + - Converters and validators run when attributes are set by default -- if + *frozen* is `False`. + - *slots=True* + + .. caution:: + + Usually this has only upsides and few visible effects in everyday + programming. But it *can* lead to some surprising behaviors, so please + make sure to read :term:`slotted classes`. - *auto_exc=True* - *auto_detect=True* - *order=False* - - *match_args=True* - Some options that were only relevant on Python 2 or were kept around for backwards-compatibility have been removed. @@ -77,6 +82,8 @@ def define( .. versionadded:: 20.1.0 .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). """ def do_it(cls, auto_attribs): @@ -85,6 +92,7 @@ def define( these=these, repr=repr, hash=hash, + unsafe_hash=unsafe_hash, init=init, slots=slots, frozen=frozen, @@ -123,10 +131,8 @@ def define( for base_cls in cls.__bases__: if base_cls.__setattr__ is _frozen_setattrs: if had_on_setattr: - raise ValueError( - "Frozen classes can't use on_setattr " - "(frozen-ness was inherited)." - ) + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) on_setattr = setters.NO_OP break @@ -143,8 +149,8 @@ def define( # if it's used as `@attrs` but ``None`` if used as `@attrs()`. if maybe_cls is None: return wrap - else: - return wrap(maybe_cls) + + return wrap(maybe_cls) mutable = define @@ -159,17 +165,22 @@ def field( hash=None, init=True, metadata=None, + type=None, converter=None, factory=None, kw_only=False, eq=None, order=None, on_setattr=None, + alias=None, ): """ Identical to `attr.ib`, except keyword-only and with some arguments removed. + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. .. versionadded:: 20.1.0 """ return attrib( @@ -179,12 +190,14 @@ def field( hash=hash, init=init, metadata=metadata, + type=type, converter=converter, factory=factory, kw_only=kw_only, eq=eq, order=order, on_setattr=on_setattr, + alias=alias, ) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_typing_compat.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/_typing_compat.pyi new file mode 100644 index 00000000000..ca7b71e906a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/_version_info.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/_version_info.py index cdaeec37a1a..51a1312f975 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/_version_info.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/_version_info.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function from functools import total_ordering @@ -10,7 +9,7 @@ from ._make import attrib, attrs @total_ordering @attrs(eq=False, order=False, slots=True, frozen=True) -class VersionInfo(object): +class VersionInfo: """ A version object that can be compared to tuple of length 1--4: diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.py index 1fb6c05d7bb..2bf4c902a66 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.py @@ -4,15 +4,11 @@ Commonly useful converters. """ -from __future__ import absolute_import, division, print_function - -from ._compat import PY2 -from ._make import NOTHING, Factory, pipe +import typing -if not PY2: - import inspect - import typing +from ._compat import _AnnotationExtractor +from ._make import NOTHING, Factory, pipe __all__ = [ @@ -42,22 +38,15 @@ def optional(converter): return None return converter(val) - if not PY2: - sig = None - try: - sig = inspect.signature(converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if params and params[0].annotation is not inspect.Parameter.empty: - optional_converter.__annotations__["val"] = typing.Optional[ - params[0].annotation - ] - if sig.return_annotation is not inspect.Signature.empty: - optional_converter.__annotations__["return"] = typing.Optional[ - sig.return_annotation - ] + xtr = _AnnotationExtractor(converter) + + t = xtr.get_first_param_type() + if t: + optional_converter.__annotations__["val"] = typing.Optional[t] + + rt = xtr.get_return_type() + if rt: + optional_converter.__annotations__["return"] = typing.Optional[rt] return optional_converter @@ -81,21 +70,20 @@ def default_if_none(default=NOTHING, factory=None): .. versionadded:: 18.2.0 """ if default is NOTHING and factory is None: - raise TypeError("Must pass either `default` or `factory`.") + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) if default is not NOTHING and factory is not None: - raise TypeError( - "Must pass either `default` or `factory` but not both." - ) + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) if factory is not None: default = Factory(factory) if isinstance(default, Factory): if default.takes_self: - raise ValueError( - "`takes_self` is not supported by default_if_none." - ) + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) def default_if_none_converter(val): if val is not None: @@ -152,4 +140,5 @@ def to_bool(val): except TypeError: # Raised when "val" is not hashable (e.g., lists) pass - raise ValueError("Cannot convert value to bool: {}".format(val)) + msg = f"Cannot convert value to bool: {val}" + raise ValueError(msg) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.pyi index 0f58088a37b..5abb49f6d5a 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/converters.pyi @@ -1,4 +1,4 @@ -from typing import Callable, Optional, TypeVar, overload +from typing import Callable, TypeVar, overload from . import _ConverterType diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/exceptions.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/exceptions.py index b2f1edc32a9..3b7abb81541 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/exceptions.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/exceptions.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function +from __future__ import annotations + +from typing import ClassVar class FrozenError(AttributeError): @@ -15,7 +17,7 @@ class FrozenError(AttributeError): """ msg = "can't set attribute" - args = [msg] + args: ClassVar[tuple[str]] = [msg] class FrozenInstanceError(FrozenError): @@ -36,7 +38,7 @@ class FrozenAttributeError(FrozenError): class AttrsAttributeNotFoundError(ValueError): """ - An ``attrs`` function couldn't find an attribute that the user asked for. + An *attrs* function couldn't find an attribute that the user asked for. .. versionadded:: 16.2.0 """ @@ -44,7 +46,7 @@ class AttrsAttributeNotFoundError(ValueError): class NotAnAttrsClassError(ValueError): """ - A non-``attrs`` class has been passed into an ``attrs`` function. + A non-*attrs* class has been passed into an *attrs* function. .. versionadded:: 16.2.0 """ @@ -52,7 +54,7 @@ class NotAnAttrsClassError(ValueError): class DefaultAlreadySetError(RuntimeError): """ - A default has been set using ``attr.ib()`` and is attempted to be reset + A default has been set when defining the field and is attempted to be reset using the decorator. .. versionadded:: 17.1.0 @@ -61,8 +63,7 @@ class DefaultAlreadySetError(RuntimeError): class UnannotatedAttributeError(RuntimeError): """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. + A class with ``auto_attribs=True`` has a field without a type annotation. .. versionadded:: 17.3.0 """ @@ -70,7 +71,7 @@ class UnannotatedAttributeError(RuntimeError): class PythonTooOldError(RuntimeError): """ - It was attempted to use an ``attrs`` feature that requires a newer Python + It was attempted to use an *attrs* feature that requires a newer Python version. .. versionadded:: 18.2.0 @@ -79,8 +80,8 @@ class PythonTooOldError(RuntimeError): class NotCallableError(TypeError): """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. + A field requiring a callable has been set with a value that is not + callable. .. versionadded:: 19.2.0 """ diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.py index a1978a87755..a1e40c98db8 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.py @@ -4,9 +4,6 @@ Commonly useful filters for `attr.asdict`. """ -from __future__ import absolute_import, division, print_function - -from ._compat import isclass from ._make import Attribute @@ -15,7 +12,8 @@ def _split_what(what): Returns a tuple of `frozenset`s of classes and attributes. """ return ( - frozenset(cls for cls in what if isclass(cls)), + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), frozenset(cls for cls in what if isinstance(cls, Attribute)), ) @@ -25,14 +23,21 @@ def include(*what): Include *what*. :param what: What to include. - :type what: `list` of `type` or `attrs.Attribute`\\ s + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s :rtype: `callable` + + .. versionchanged:: 23.1.0 Accept strings with field names. """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def include_(attribute, value): - return value.__class__ in cls or attribute in attrs + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return include_ @@ -42,13 +47,20 @@ def exclude(*what): Exclude *what*. :param what: What to exclude. - :type what: `list` of classes or `attrs.Attribute`\\ s. + :type what: `list` of classes `type`, field names `str` or + `attrs.Attribute`\\ s. :rtype: `callable` + + .. versionchanged:: 23.3.0 Accept field name string as input argument """ - cls, attrs = _split_what(what) + cls, names, attrs = _split_what(what) def exclude_(attribute, value): - return value.__class__ not in cls and attribute not in attrs + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) return exclude_ diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.pyi index 993866865ea..8a02fa0fc06 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/filters.pyi @@ -2,5 +2,5 @@ from typing import Any, Union from . import Attribute, _FilterType -def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def include(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, str, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.py index b1cbb5d83e7..12ed6750df3 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.py @@ -4,7 +4,6 @@ Commonly used hooks for on_setattr. """ -from __future__ import absolute_import, division, print_function from . import _config from .exceptions import FrozenAttributeError @@ -69,11 +68,6 @@ def convert(instance, attrib, new_value): return new_value +# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. +# autodata stopped working, so the docstring is inlined in the API docs. NO_OP = object() -""" -Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. - -Does not work in `pipe` or within lists. - -.. versionadded:: 20.1.0 -""" diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.pyi index 3f5603c2b0c..72f7ce4761c 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/setters.pyi @@ -1,4 +1,4 @@ -from typing import Any, NewType, NoReturn, TypeVar, cast +from typing import Any, NewType, NoReturn, TypeVar from . import Attribute, _OnSetAttrType diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.py b/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.py index 0b0c8342f25..34d6b761d37 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.py @@ -4,24 +4,19 @@ Commonly useful validators. """ -from __future__ import absolute_import, division, print_function import operator import re from contextlib import contextmanager +from re import Pattern from ._config import get_run_validators, set_run_validators from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none from .exceptions import NotCallableError -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - __all__ = [ "and_", "deep_iterable", @@ -37,6 +32,8 @@ __all__ = [ "lt", "matches_re", "max_len", + "min_len", + "not_", "optional", "provides", "set_disabled", @@ -92,7 +89,7 @@ def disabled(): @attrs(repr=False, slots=True, hash=True) -class _InstanceOfValidator(object): +class _InstanceOfValidator: type = attrib() def __call__(self, inst, attr, value): @@ -100,23 +97,21 @@ class _InstanceOfValidator(object): We use a callable class to be able to change the ``__repr__``. """ if not isinstance(value, self.type): + msg = "'{name}' must be {type!r} (got {value!r} that is a {actual!r}).".format( + name=attr.name, + type=self.type, + actual=value.__class__, + value=value, + ) raise TypeError( - "'{name}' must be {type!r} (got {value!r} that is a " - "{actual!r}).".format( - name=attr.name, - type=self.type, - actual=value.__class__, - value=value, - ), + msg, attr, self.type, value, ) def __repr__(self): - return "<instance_of validator for type {type!r}>".format( - type=self.type - ) + return f"<instance_of validator for type {self.type!r}>" def instance_of(type): @@ -126,7 +121,7 @@ def instance_of(type): `isinstance` therefore it's also valid to pass a tuple of types). :param type: The type to check for. - :type type: type or tuple of types + :type type: type or tuple of type :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected type, and the value it @@ -136,7 +131,7 @@ def instance_of(type): @attrs(repr=False, frozen=True, slots=True) -class _MatchesReValidator(object): +class _MatchesReValidator: pattern = attrib() match_func = attrib() @@ -145,20 +140,18 @@ class _MatchesReValidator(object): We use a callable class to be able to change the ``__repr__``. """ if not self.match_func(value): + msg = "'{name}' must match regex {pattern!r} ({value!r} doesn't)".format( + name=attr.name, pattern=self.pattern.pattern, value=value + ) raise ValueError( - "'{name}' must match regex {pattern!r}" - " ({value!r} doesn't)".format( - name=attr.name, pattern=self.pattern.pattern, value=value - ), + msg, attr, self.pattern, value, ) def __repr__(self): - return "<matches_re validator for pattern {pattern!r}>".format( - pattern=self.pattern - ) + return f"<matches_re validator for pattern {self.pattern!r}>" def matches_re(regex, flags=0, func=None): @@ -169,34 +162,27 @@ def matches_re(regex, flags=0, func=None): :param regex: a regex string or precompiled pattern to match against :param int flags: flags that will be passed to the underlying re function (default 0) - :param callable func: which underlying `re` function to call (options - are `re.fullmatch`, `re.search`, `re.match`, default - is ``None`` which means either `re.fullmatch` or an emulation of - it on Python 2). For performance reasons, they won't be used directly - but on a pre-`re.compile`\ ed pattern. + :param callable func: which underlying `re` function to call. Valid options + are `re.fullmatch`, `re.search`, and `re.match`; the default ``None`` + means `re.fullmatch`. For performance reasons, the pattern is always + precompiled using `re.compile`. .. versionadded:: 19.2.0 .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. """ - fullmatch = getattr(re, "fullmatch", None) - valid_funcs = (fullmatch, None, re.search, re.match) + valid_funcs = (re.fullmatch, None, re.search, re.match) if func not in valid_funcs: - raise ValueError( - "'func' must be one of {}.".format( - ", ".join( - sorted( - e and e.__name__ or "None" for e in set(valid_funcs) - ) - ) + msg = "'func' must be one of {}.".format( + ", ".join( + sorted(e and e.__name__ or "None" for e in set(valid_funcs)) ) ) + raise ValueError(msg) if isinstance(regex, Pattern): if flags: - raise TypeError( - "'flags' can only be used with a string pattern; " - "pass flags to re.compile() instead" - ) + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) pattern = regex else: pattern = re.compile(regex, flags) @@ -205,19 +191,14 @@ def matches_re(regex, flags=0, func=None): match_func = pattern.match elif func is re.search: match_func = pattern.search - elif fullmatch: + else: match_func = pattern.fullmatch - else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) - pattern = re.compile( - r"(?:{})\Z".format(pattern.pattern), pattern.flags - ) - match_func = pattern.match return _MatchesReValidator(pattern, match_func) @attrs(repr=False, slots=True, hash=True) -class _ProvidesValidator(object): +class _ProvidesValidator: interface = attrib() def __call__(self, inst, attr, value): @@ -225,20 +206,18 @@ class _ProvidesValidator(object): We use a callable class to be able to change the ``__repr__``. """ if not self.interface.providedBy(value): + msg = "'{name}' must provide {interface!r} which {value!r} doesn't.".format( + name=attr.name, interface=self.interface, value=value + ) raise TypeError( - "'{name}' must provide {interface!r} which {value!r} " - "doesn't.".format( - name=attr.name, interface=self.interface, value=value - ), + msg, attr, self.interface, value, ) def __repr__(self): - return "<provides validator for interface {interface!r}>".format( - interface=self.interface - ) + return f"<provides validator for interface {self.interface!r}>" def provides(interface): @@ -254,12 +233,22 @@ def provides(interface): :raises TypeError: With a human readable error message, the attribute (of type `attrs.Attribute`), the expected interface, and the value it got. + + .. deprecated:: 23.1.0 """ + import warnings + + warnings.warn( + "attrs's zope-interface support is deprecated and will be removed in, " + "or after, April 2024.", + DeprecationWarning, + stacklevel=2, + ) return _ProvidesValidator(interface) @attrs(repr=False, slots=True, hash=True) -class _OptionalValidator(object): +class _OptionalValidator: validator = attrib() def __call__(self, inst, attr, value): @@ -269,9 +258,7 @@ class _OptionalValidator(object): self.validator(inst, attr, value) def __repr__(self): - return "<optional validator for {what} or None>".format( - what=repr(self.validator) - ) + return f"<optional validator for {self.validator!r} or None>" def optional(validator): @@ -280,20 +267,21 @@ def optional(validator): which can be set to ``None`` in addition to satisfying the requirements of the sub-validator. - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. + :param Callable | tuple[Callable] | list[Callable] validator: A validator + (or validators) that is used for non-``None`` values. .. versionadded:: 15.1.0 .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. """ - if isinstance(validator, list): + if isinstance(validator, (list, tuple)): return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) @attrs(repr=False, slots=True, hash=True) -class _InValidator(object): +class _InValidator: options = attrib() def __call__(self, inst, attr, value): @@ -303,16 +291,16 @@ class _InValidator(object): in_options = False if not in_options: + msg = f"'{attr.name}' must be in {self.options!r} (got {value!r})" raise ValueError( - "'{name}' must be in {options!r} (got {value!r})".format( - name=attr.name, options=self.options, value=value - ) + msg, + attr, + self.options, + value, ) def __repr__(self): - return "<in_ validator with options {options!r}>".format( - options=self.options - ) + return f"<in_ validator with options {self.options!r}>" def in_(options): @@ -329,12 +317,16 @@ def in_(options): got. .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. """ return _InValidator(options) @attrs(repr=False, slots=False, hash=True) -class _IsCallableValidator(object): +class _IsCallableValidator: def __call__(self, inst, attr, value): """ We use a callable class to be able to change the ``__repr__``. @@ -357,13 +349,13 @@ class _IsCallableValidator(object): def is_callable(): """ - A validator that raises a `attr.exceptions.NotCallableError` if the + A validator that raises a `attrs.exceptions.NotCallableError` if the initializer is called with a value for this particular attribute that is not callable. .. versionadded:: 19.1.0 - :raises `attr.exceptions.NotCallableError`: With a human readable error + :raises attrs.exceptions.NotCallableError: With a human readable error message containing the attribute (`attrs.Attribute`) name, and the value it got. """ @@ -371,7 +363,7 @@ def is_callable(): @attrs(repr=False, slots=True, hash=True) -class _DeepIterable(object): +class _DeepIterable: member_validator = attrib(validator=is_callable()) iterable_validator = attrib( default=None, validator=optional(is_callable()) @@ -391,14 +383,11 @@ class _DeepIterable(object): iterable_identifier = ( "" if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) + else f" {self.iterable_validator!r}" ) return ( - "<deep_iterable validator for{iterable_identifier}" - " iterables of {member!r}>" - ).format( - iterable_identifier=iterable_identifier, - member=self.member_validator, + f"<deep_iterable validator for{iterable_identifier}" + f" iterables of {self.member_validator!r}>" ) @@ -406,7 +395,7 @@ def deep_iterable(member_validator, iterable_validator=None): """ A validator that performs deep validation of an iterable. - :param member_validator: Validator to apply to iterable members + :param member_validator: Validator(s) to apply to iterable members :param iterable_validator: Validator to apply to iterable itself (optional) @@ -414,11 +403,13 @@ def deep_iterable(member_validator, iterable_validator=None): :raises TypeError: if any sub-validators fail """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) return _DeepIterable(member_validator, iterable_validator) @attrs(repr=False, slots=True, hash=True) -class _DeepMapping(object): +class _DeepMapping: key_validator = attrib(validator=is_callable()) value_validator = attrib(validator=is_callable()) mapping_validator = attrib(default=None, validator=optional(is_callable())) @@ -457,7 +448,7 @@ def deep_mapping(key_validator, value_validator, mapping_validator=None): @attrs(repr=False, frozen=True, slots=True) -class _NumberValidator(object): +class _NumberValidator: bound = attrib() compare_op = attrib() compare_func = attrib() @@ -467,19 +458,11 @@ class _NumberValidator(object): We use a callable class to be able to change the ``__repr__``. """ if not self.compare_func(value, self.bound): - raise ValueError( - "'{name}' must be {op} {bound}: {value}".format( - name=attr.name, - op=self.compare_op, - bound=self.bound, - value=value, - ) - ) + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) def __repr__(self): - return "<Validator for x {op} {bound}>".format( - op=self.compare_op, bound=self.bound - ) + return f"<Validator for x {self.compare_op} {self.bound}>" def lt(val): @@ -531,7 +514,7 @@ def gt(val): @attrs(repr=False, frozen=True, slots=True) -class _MaxLengthValidator(object): +class _MaxLengthValidator: max_length = attrib() def __call__(self, inst, attr, value): @@ -539,14 +522,11 @@ class _MaxLengthValidator(object): We use a callable class to be able to change the ``__repr__``. """ if len(value) > self.max_length: - raise ValueError( - "Length of '{name}' must be <= {max}: {len}".format( - name=attr.name, max=self.max_length, len=len(value) - ) - ) + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) def __repr__(self): - return "<max_len validator for {max}>".format(max=self.max_length) + return f"<max_len validator for {self.max_length}>" def max_len(length): @@ -559,3 +539,143 @@ def max_len(length): .. versionadded:: 21.3.0 """ return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator: + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"<min_len validator for {self.min_length}>" + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + :param int length: Minimum length of the string or iterable + + .. versionadded:: 22.1.0 + """ + return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"<subclass_of validator for type {self.type!r}>" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attrs.Attribute`), the expected type, and the value it + got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return ( + "<not_ validator wrapping {what!r}, capturing {exc_types!r}>" + ).format( + what=self.validator, + exc_types=self.exc_types, + ) + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + :param validator: A validator to be logically inverted. + :param msg: Message to raise if validator fails. + Formatted with keys ``exc_types`` and ``validator``. + :type msg: str + :param exc_types: Exception type(s) to capture. + Other types raised by child validators will not be intercepted and + pass through. + + :raises ValueError: With a human readable error message, + the attribute (of type `attrs.Attribute`), + the validator that failed to raise an exception, + the value it got, + and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.pyi index 5e00b854339..d194a75abca 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attr/validators.pyi @@ -18,6 +18,7 @@ from typing import ( ) from . import _ValidatorType +from . import _ValidatorArgType _T = TypeVar("_T") _T1 = TypeVar("_T1") @@ -50,7 +51,9 @@ def instance_of( def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] + validator: Union[ + _ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T]] + ] ) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... @@ -62,7 +65,7 @@ def matches_re( ] = ..., ) -> _ValidatorType[AnyStr]: ... def deep_iterable( - member_validator: _ValidatorType[_T], + member_validator: _ValidatorArgType[_T], iterable_validator: Optional[_ValidatorType[_I]] = ..., ) -> _ValidatorType[_I]: ... def deep_mapping( @@ -76,3 +79,10 @@ def le(val: _T) -> _ValidatorType[_T]: ... def ge(val: _T) -> _ValidatorType[_T]: ... def gt(val: _T) -> _ValidatorType[_T]: ... def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: Optional[str] = None, + exc_types: Union[Type[Exception], Iterable[Type[Exception]]] = ..., +) -> _ValidatorType[_T]: ... diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.py index a704b8b56bc..0c2481561a9 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.py @@ -3,17 +3,9 @@ from attr import ( NOTHING, Attribute, + AttrsInstance, Factory, - __author__, - __copyright__, - __description__, - __doc__, - __email__, - __license__, - __title__, - __url__, - __version__, - __version_info__, + _make_getattr, assoc, cmp_using, define, @@ -48,6 +40,7 @@ __all__ = [ "assoc", "astuple", "Attribute", + "AttrsInstance", "cmp_using", "converters", "define", @@ -68,3 +61,5 @@ __all__ = [ "validate", "validators", ] + +__getattr__ = _make_getattr(__name__) diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.pyi b/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.pyi index 7426fa5ddbf..9372cfea16e 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.pyi +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/__init__.pyi @@ -23,13 +23,17 @@ from attr import __version_info__ as __version_info__ from attr import _FilterType from attr import assoc as assoc from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance +from attr import cmp_using as cmp_using +from attr import converters as converters from attr import define as define from attr import evolve as evolve -from attr import Factory as Factory from attr import exceptions as exceptions +from attr import Factory as Factory from attr import field as field from attr import fields as fields from attr import fields_dict as fields_dict +from attr import filters as filters from attr import frozen as frozen from attr import has as has from attr import make_class as make_class @@ -42,7 +46,7 @@ from attr import validators as validators # TODO: see definition of attr.asdict/astuple def asdict( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., dict_factory: Type[Mapping[Any, Any]] = ..., @@ -55,7 +59,7 @@ def asdict( # TODO: add support for returning NamedTuple from the mypy plugin def astuple( - inst: Any, + inst: AttrsInstance, recurse: bool = ..., filter: Optional[_FilterType[Any]] = ..., tuple_factory: Type[Sequence[Any]] = ..., diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/converters.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/converters.py index edfa8d3c16a..7821f6c02cc 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/converters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/converters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.converters import * # noqa +from attr.converters import * # noqa: F403 diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/exceptions.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/exceptions.py index bd9efed202a..3323f9d2112 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/exceptions.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/exceptions.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.exceptions import * # noqa +from attr.exceptions import * # noqa: F403 diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/filters.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/filters.py index 52959005b08..3080f48398e 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/filters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/filters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.filters import * # noqa +from attr.filters import * # noqa: F403 diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/setters.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/setters.py index 9b50770804e..f3d73bb793d 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/setters.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/setters.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.setters import * # noqa +from attr.setters import * # noqa: F403 diff --git a/tests/wpt/tests/tools/third_party/attrs/src/attrs/validators.py b/tests/wpt/tests/tools/third_party/attrs/src/attrs/validators.py index ab2c9b30247..037e124f29f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/src/attrs/validators.py +++ b/tests/wpt/tests/tools/third_party/attrs/src/attrs/validators.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: MIT -from attr.validators import * # noqa +from attr.validators import * # noqa: F403 diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/attr_import_star.py b/tests/wpt/tests/tools/third_party/attrs/tests/attr_import_star.py index eaec321bac4..bdc5c091b7f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/attr_import_star.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/attr_import_star.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import -from attr import * # noqa: F401,F403 +from attr import * # noqa: F403 # This is imported by test_import::test_from_attr_import_star; this must diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/dataclass_transform_example.py b/tests/wpt/tests/tools/third_party/attrs/tests/dataclass_transform_example.py index 49e09061a8a..c65df14026d 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/dataclass_transform_example.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/dataclass_transform_example.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT import attr +import attrs @attr.define() @@ -9,7 +10,7 @@ class Define: b: int -reveal_type(Define.__init__) # noqa +reveal_type(Define.__init__) # noqa: F821 @attr.define() @@ -18,10 +19,11 @@ class DefineConverter: with_converter: int = attr.field(converter=int) -reveal_type(DefineConverter.__init__) # noqa +reveal_type(DefineConverter.__init__) # noqa: F821 + +DefineConverter(with_converter=b"42") -# mypy plugin supports attr.frozen, pyright does not @attr.frozen() class Frozen: a: str @@ -30,10 +32,9 @@ class Frozen: d = Frozen("a") d.a = "new" -reveal_type(d.a) # noqa +reveal_type(d.a) # noqa: F821 -# but pyright supports attr.define(frozen) @attr.define(frozen=True) class FrozenDefine: a: str @@ -42,4 +43,21 @@ class FrozenDefine: d2 = FrozenDefine("a") d2.a = "new" -reveal_type(d2.a) # noqa +reveal_type(d2.a) # noqa: F821 + + +# Field-aliasing works +@attrs.define +class AliasedField: + _a: int = attrs.field(alias="_a") + + +af = AliasedField(42) + +reveal_type(af.__init__) # noqa: F821 + + +# unsafe_hash is accepted +@attrs.define(unsafe_hash=True) +class Hashable: + pass diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/strategies.py b/tests/wpt/tests/tools/third_party/attrs/tests/strategies.py index 99f9f48536b..783058f837f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/strategies.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/strategies.py @@ -3,7 +3,7 @@ """ Testing strategies for Hypothesis-based tests. """ - +import functools import keyword import string @@ -13,6 +13,8 @@ from hypothesis import strategies as st import attr +from attr._compat import PY_3_8_PLUS + from .utils import make_class @@ -28,8 +30,7 @@ def gen_attr_names(): Some short strings (such as 'as') are keywords, so we skip them. """ lc = string.ascii_lowercase - for c in lc: - yield c + yield from lc for outer in lc: for inner in lc: res = outer + inner @@ -67,7 +68,7 @@ def _create_hyp_nested_strategy(draw, simple_class_strategy): lambda: OrderedDict([("cls", cls())]), ] factory = draw(st.sampled_from(factories)) - attrs = draw(list_of_attrs) + [attr.ib(default=attr.Factory(factory))] + attrs = [*draw(list_of_attrs), attr.ib(default=attr.Factory(factory))] return make_class("HypClass", dict(zip(gen_attr_names(), attrs))) @@ -112,13 +113,19 @@ def simple_attrs_with_metadata(draw): simple_attrs = simple_attrs_without_metadata | simple_attrs_with_metadata() + # Python functions support up to 255 arguments. list_of_attrs = st.lists(simple_attrs, max_size=3) @st.composite def simple_classes( - draw, slots=None, frozen=None, weakref_slot=None, private_attrs=None + draw, + slots=None, + frozen=None, + weakref_slot=None, + private_attrs=None, + cached_property=None, ): """ A strategy that generates classes with default non-attr attributes. @@ -158,6 +165,7 @@ def simple_classes( pre_init_flag = draw(st.booleans()) post_init_flag = draw(st.booleans()) init_flag = draw(st.booleans()) + cached_property_flag = draw(st.booleans()) if pre_init_flag: @@ -180,9 +188,22 @@ def simple_classes( cls_dict["__init__"] = init + bases = (object,) + if cached_property or ( + PY_3_8_PLUS and cached_property is None and cached_property_flag + ): + + class BaseWithCachedProperty: + @functools.cached_property + def _cached_property(self) -> int: + return 1 + + bases = (BaseWithCachedProperty,) + return make_class( "HypClass", cls_dict, + bases=bases, slots=slots_flag if slots is None else slots, frozen=frozen_flag if frozen is None else frozen, weakref_slot=weakref_flag if weakref_slot is None else weakref_slot, diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_3rd_party.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_3rd_party.py index 8866d7f6ef2..b2ce06c293b 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_3rd_party.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_3rd_party.py @@ -14,12 +14,12 @@ from .strategies import simple_classes cloudpickle = pytest.importorskip("cloudpickle") -class TestCloudpickleCompat(object): +class TestCloudpickleCompat: """ Tests for compatibility with ``cloudpickle``. """ - @given(simple_classes()) + @given(simple_classes(cached_property=False)) def test_repr(self, cls): """ attrs instances can be pickled and un-pickled with cloudpickle. diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_abc.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_abc.py new file mode 100644 index 00000000000..a70b317a3ce --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_abc.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: MIT + +import abc +import inspect + +import pytest + +import attrs + +from attr._compat import PY310, PY_3_12_PLUS + + +@pytest.mark.skipif(not PY310, reason="abc.update_abstractmethods is 3.10+") +class TestUpdateAbstractMethods: + def test_abc_implementation(self, slots): + """ + If an attrs class implements an abstract method, it stops being + abstract. + """ + + class Ordered(abc.ABC): + @abc.abstractmethod + def __lt__(self, other): + pass + + @abc.abstractmethod + def __le__(self, other): + pass + + @attrs.define(order=True, slots=slots) + class Concrete(Ordered): + x: int + + assert not inspect.isabstract(Concrete) + assert Concrete(2) > Concrete(1) + + def test_remain_abstract(self, slots): + """ + If an attrs class inherits from an abstract class but doesn't implement + abstract methods, it remains abstract. + """ + + class A(abc.ABC): + @abc.abstractmethod + def foo(self): + pass + + @attrs.define(slots=slots) + class StillAbstract(A): + pass + + assert inspect.isabstract(StillAbstract) + expected_exception_message = ( + "^Can't instantiate abstract class StillAbstract without an " + "implementation for abstract method 'foo'$" + if PY_3_12_PLUS + else "class StillAbstract with abstract method foo" + ) + with pytest.raises(TypeError, match=expected_exception_message): + StillAbstract() diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_annotations.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_annotations.py index a201ebf7fa6..d27d9e3743f 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_annotations.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_annotations.py @@ -2,8 +2,6 @@ """ Tests for PEP-526 type annotations. - -Python 3.6+ only. """ import sys @@ -94,7 +92,10 @@ class TestAnnotations: assert 1 == len(attr.fields(C)) assert_init_annotations(C, x=typing.List[int]) - @pytest.mark.parametrize("slots", [True, False]) + @pytest.mark.skipif( + sys.version_info[:2] < (3, 11), + reason="Incompatible behavior on older Pythons", + ) def test_auto_attribs(self, slots): """ If *auto_attribs* is True, bare annotations are collected too. @@ -113,7 +114,7 @@ class TestAnnotations: i = C(42) assert "C(a=42, x=[], y=2, z=3, foo=None)" == repr(i) - attr_names = set(a.name for a in C.__attrs_attrs__) + attr_names = {a.name for a in C.__attrs_attrs__} assert "a" in attr_names # just double check that the set works assert "cls_var" not in attr_names @@ -149,10 +150,9 @@ class TestAnnotations: x=typing.List[int], y=int, z=int, - foo=typing.Optional[typing.Any], + foo=typing.Any, ) - @pytest.mark.parametrize("slots", [True, False]) def test_auto_attribs_unannotated(self, slots): """ Unannotated `attr.ib`s raise an error. @@ -170,7 +170,6 @@ class TestAnnotations: "The following `attr.ib`s lack a type annotation: v, y.", ) == e.value.args - @pytest.mark.parametrize("slots", [True, False]) def test_auto_attribs_subclassing(self, slots): """ Attributes from base classes are inherited, it doesn't matter if the @@ -251,7 +250,7 @@ class TestAnnotations: def test_nullary_converter(self): """ - A coverter with no arguments doesn't cause a crash. + A converter with no arguments doesn't cause a crash. """ def noop(): @@ -384,15 +383,15 @@ class TestAnnotations: assert attr.converters.optional(noop).__annotations__ == {} - @pytest.mark.xfail( - sys.version_info[:2] == (3, 6), reason="Does not work on 3.6." + @pytest.mark.skipif( + sys.version_info[:2] < (3, 11), + reason="Incompatible behavior on older Pythons", ) - @pytest.mark.parametrize("slots", [True, False]) def test_annotations_strings(self, slots): """ String annotations are passed into __init__ as is. - It fails on 3.6 due to a bug in Python. + The strings keep changing between releases. """ import typing as t @@ -417,10 +416,9 @@ class TestAnnotations: x=typing.List[int], y=int, z=int, - foo=typing.Optional[typing.Any], + foo=typing.Any, ) - @pytest.mark.parametrize("slots", [True, False]) def test_typing_extensions_classvar(self, slots): """ If ClassVar is coming from typing_extensions, it is recognized too. @@ -428,7 +426,7 @@ class TestAnnotations: @attr.s(auto_attribs=True, slots=slots) class C: - cls_var: "typing_extensions.ClassVar" = 23 # noqa + cls_var: "typing_extensions.ClassVar" = 23 # noqa: F821 assert_init_annotations(C) @@ -518,7 +516,34 @@ class TestAnnotations: assert str is attr.fields(C).y.type assert None is attr.fields(C).z.type - @pytest.mark.parametrize("slots", [True, False]) + @pytest.mark.skipif( + sys.version_info[:2] < (3, 9), + reason="Incompatible behavior on older Pythons", + ) + def test_extra_resolve(self): + """ + `get_type_hints` returns extra type hints. + """ + from typing import Annotated + + globals = {"Annotated": Annotated} + + @attr.define + class C: + x: 'Annotated[float, "test"]' + + attr.resolve_types(C, globals) + + assert attr.fields(C).x.type == Annotated[float, "test"] + + @attr.define + class D: + x: 'Annotated[float, "test"]' + + attr.resolve_types(D, globals, include_extras=False) + + assert attr.fields(D).x.type == float + def test_resolve_types_auto_attrib(self, slots): """ Types can be resolved even when strings are involved. @@ -538,7 +563,6 @@ class TestAnnotations: assert typing.List[int] == attr.fields(A).b.type assert typing.List[int] == attr.fields(A).c.type - @pytest.mark.parametrize("slots", [True, False]) def test_resolve_types_decorator(self, slots): """ Types can be resolved using it as a decorator. @@ -555,7 +579,6 @@ class TestAnnotations: assert typing.List[int] == attr.fields(A).b.type assert typing.List[int] == attr.fields(A).c.type - @pytest.mark.parametrize("slots", [True, False]) def test_self_reference(self, slots): """ References to self class using quotes can be resolved. @@ -564,14 +587,13 @@ class TestAnnotations: @attr.s(slots=slots, auto_attribs=True) class A: a: "A" - b: typing.Optional["A"] # noqa: will resolve below + b: typing.Optional["A"] # will resolve below -- noqa: F821 attr.resolve_types(A, globals(), locals()) assert A == attr.fields(A).a.type assert typing.Optional[A] == attr.fields(A).b.type - @pytest.mark.parametrize("slots", [True, False]) def test_forward_reference(self, slots): """ Forward references can be resolved. @@ -579,7 +601,7 @@ class TestAnnotations: @attr.s(slots=slots, auto_attribs=True) class A: - a: typing.List["B"] # noqa: will resolve below + a: typing.List["B"] # will resolve below -- noqa: F821 @attr.s(slots=slots, auto_attribs=True) class B: diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_cmp.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_cmp.py index ec2c6874899..07bfc5234ad 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_cmp.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_cmp.py @@ -4,12 +4,10 @@ Tests for methods from `attrib._cmp`. """ -from __future__ import absolute_import, division, print_function import pytest from attr._cmp import cmp_using -from attr._compat import PY2 # Test parameters. @@ -57,7 +55,7 @@ cmp_data = eq_data + order_data cmp_ids = eq_ids + order_ids -class TestEqOrder(object): +class TestEqOrder: """ Tests for eq and order related methods. """ @@ -65,7 +63,9 @@ class TestEqOrder(object): ######### # eq ######### - @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), cmp_data, ids=cmp_ids + ) def test_equal_same_type(self, cls, requires_same_type): """ Equal objects are detected as equal. @@ -73,7 +73,9 @@ class TestEqOrder(object): assert cls(1) == cls(1) assert not (cls(1) != cls(1)) - @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), cmp_data, ids=cmp_ids + ) def test_unequal_same_type(self, cls, requires_same_type): """ Unequal objects of correct type are detected as unequal. @@ -81,7 +83,9 @@ class TestEqOrder(object): assert cls(1) != cls(2) assert not (cls(1) == cls(2)) - @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), cmp_data, ids=cmp_ids + ) def test_equal_different_type(self, cls, requires_same_type): """ Equal values of different types are detected appropriately. @@ -92,8 +96,9 @@ class TestEqOrder(object): ######### # lt ######### - @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError") - @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), eq_data, ids=eq_ids + ) def test_lt_unorderable(self, cls, requires_same_type): """ TypeError is raised if class does not implement __lt__. @@ -102,7 +107,7 @@ class TestEqOrder(object): cls(1) < cls(2) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_lt_same_type(self, cls, requires_same_type): """ @@ -112,7 +117,7 @@ class TestEqOrder(object): assert not (cls(2) < cls(1)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_not_lt_same_type(self, cls, requires_same_type): """ @@ -122,7 +127,7 @@ class TestEqOrder(object): assert not (cls(1) >= cls(2)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_lt_different_type(self, cls, requires_same_type): """ @@ -131,9 +136,8 @@ class TestEqOrder(object): if requires_same_type: # Unlike __eq__, NotImplemented will cause an exception to be # raised from __lt__. - if not PY2: - with pytest.raises(TypeError): - cls(1) < cls(2.0) + with pytest.raises(TypeError): + cls(1) < cls(2.0) else: assert cls(1) < cls(2.0) assert not (cls(2) < cls(1.0)) @@ -141,8 +145,9 @@ class TestEqOrder(object): ######### # le ######### - @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError") - @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), eq_data, ids=eq_ids + ) def test_le_unorderable(self, cls, requires_same_type): """ TypeError is raised if class does not implement __le__. @@ -151,7 +156,7 @@ class TestEqOrder(object): cls(1) <= cls(2) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_le_same_type(self, cls, requires_same_type): """ @@ -162,7 +167,7 @@ class TestEqOrder(object): assert not (cls(2) <= cls(1)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_not_le_same_type(self, cls, requires_same_type): """ @@ -173,7 +178,7 @@ class TestEqOrder(object): assert not (cls(1) > cls(2)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_le_different_type(self, cls, requires_same_type): """ @@ -182,9 +187,8 @@ class TestEqOrder(object): if requires_same_type: # Unlike __eq__, NotImplemented will cause an exception to be # raised from __le__. - if not PY2: - with pytest.raises(TypeError): - cls(1) <= cls(2.0) + with pytest.raises(TypeError): + cls(1) <= cls(2.0) else: assert cls(1) <= cls(2.0) assert cls(1) <= cls(1.0) @@ -193,8 +197,9 @@ class TestEqOrder(object): ######### # gt ######### - @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError") - @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), eq_data, ids=eq_ids + ) def test_gt_unorderable(self, cls, requires_same_type): """ TypeError is raised if class does not implement __gt__. @@ -203,7 +208,7 @@ class TestEqOrder(object): cls(2) > cls(1) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_gt_same_type(self, cls, requires_same_type): """ @@ -213,7 +218,7 @@ class TestEqOrder(object): assert not (cls(1) > cls(2)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_not_gt_same_type(self, cls, requires_same_type): """ @@ -223,7 +228,7 @@ class TestEqOrder(object): assert not (cls(2) <= cls(1)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_gt_different_type(self, cls, requires_same_type): """ @@ -232,9 +237,8 @@ class TestEqOrder(object): if requires_same_type: # Unlike __eq__, NotImplemented will cause an exception to be # raised from __gt__. - if not PY2: - with pytest.raises(TypeError): - cls(2) > cls(1.0) + with pytest.raises(TypeError): + cls(2) > cls(1.0) else: assert cls(2) > cls(1.0) assert not (cls(1) > cls(2.0)) @@ -242,8 +246,9 @@ class TestEqOrder(object): ######### # ge ######### - @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError") - @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids) + @pytest.mark.parametrize( + ("cls", "requires_same_type"), eq_data, ids=eq_ids + ) def test_ge_unorderable(self, cls, requires_same_type): """ TypeError is raised if class does not implement __ge__. @@ -252,7 +257,7 @@ class TestEqOrder(object): cls(2) >= cls(1) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_ge_same_type(self, cls, requires_same_type): """ @@ -263,7 +268,7 @@ class TestEqOrder(object): assert not (cls(1) >= cls(2)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_not_ge_same_type(self, cls, requires_same_type): """ @@ -274,7 +279,7 @@ class TestEqOrder(object): assert not (cls(2) < cls(1)) @pytest.mark.parametrize( - "cls, requires_same_type", order_data, ids=order_ids + ("cls", "requires_same_type"), order_data, ids=order_ids ) def test_ge_different_type(self, cls, requires_same_type): """ @@ -283,16 +288,15 @@ class TestEqOrder(object): if requires_same_type: # Unlike __eq__, NotImplemented will cause an exception to be # raised from __ge__. - if not PY2: - with pytest.raises(TypeError): - cls(2) >= cls(1.0) + with pytest.raises(TypeError): + cls(2) >= cls(1.0) else: assert cls(2) >= cls(2.0) assert cls(2) >= cls(1.0) assert not (cls(1) >= cls(2.0)) -class TestDundersUnnamedClass(object): +class TestDundersUnnamedClass: """ Tests for dunder attributes of unnamed classes. """ @@ -304,8 +308,7 @@ class TestDundersUnnamedClass(object): Class name and qualified name should be well behaved. """ assert self.cls.__name__ == "Comparable" - if not PY2: - assert self.cls.__qualname__ == "Comparable" + assert self.cls.__qualname__ == "Comparable" def test_eq(self): """ @@ -327,7 +330,7 @@ class TestDundersUnnamedClass(object): assert method.__name__ == "__ne__" -class TestTotalOrderingException(object): +class TestTotalOrderingException: """ Test for exceptions related to total ordering. """ @@ -345,7 +348,7 @@ class TestTotalOrderingException(object): ) -class TestNotImplementedIsPropagated(object): +class TestNotImplementedIsPropagated: """ Test related to functions that return NotImplemented. """ @@ -361,7 +364,7 @@ class TestNotImplementedIsPropagated(object): assert C(1) != C(1) -class TestDundersPartialOrdering(object): +class TestDundersPartialOrdering: """ Tests for dunder attributes of classes with partial ordering. """ @@ -373,8 +376,7 @@ class TestDundersPartialOrdering(object): Class name and qualified name should be well behaved. """ assert self.cls.__name__ == "PartialOrderCSameType" - if not PY2: - assert self.cls.__qualname__ == "PartialOrderCSameType" + assert self.cls.__qualname__ == "PartialOrderCSameType" def test_eq(self): """ @@ -408,12 +410,9 @@ class TestDundersPartialOrdering(object): __le__ docstring and qualified name should be well behaved. """ method = self.cls.__le__ - if PY2: - assert method.__doc__ == "x.__le__(y) <==> x<=y" - else: - assert method.__doc__.strip().startswith( - "Return a <= b. Computed by @total_ordering from" - ) + assert method.__doc__.strip().startswith( + "Return a <= b. Computed by @total_ordering from" + ) assert method.__name__ == "__le__" def test_gt(self): @@ -421,12 +420,9 @@ class TestDundersPartialOrdering(object): __gt__ docstring and qualified name should be well behaved. """ method = self.cls.__gt__ - if PY2: - assert method.__doc__ == "x.__gt__(y) <==> x>y" - else: - assert method.__doc__.strip().startswith( - "Return a > b. Computed by @total_ordering from" - ) + assert method.__doc__.strip().startswith( + "Return a > b. Computed by @total_ordering from" + ) assert method.__name__ == "__gt__" def test_ge(self): @@ -434,16 +430,13 @@ class TestDundersPartialOrdering(object): __ge__ docstring and qualified name should be well behaved. """ method = self.cls.__ge__ - if PY2: - assert method.__doc__ == "x.__ge__(y) <==> x>=y" - else: - assert method.__doc__.strip().startswith( - "Return a >= b. Computed by @total_ordering from" - ) + assert method.__doc__.strip().startswith( + "Return a >= b. Computed by @total_ordering from" + ) assert method.__name__ == "__ge__" -class TestDundersFullOrdering(object): +class TestDundersFullOrdering: """ Tests for dunder attributes of classes with full ordering. """ @@ -455,8 +448,7 @@ class TestDundersFullOrdering(object): Class name and qualified name should be well behaved. """ assert self.cls.__name__ == "FullOrderCSameType" - if not PY2: - assert self.cls.__qualname__ == "FullOrderCSameType" + assert self.cls.__qualname__ == "FullOrderCSameType" def test_eq(self): """ diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_compat.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_compat.py index 464b492f0fa..c8015b596e2 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_compat.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_compat.py @@ -1,18 +1,20 @@ # SPDX-License-Identifier: MIT +import types + import pytest -from attr._compat import metadata_proxy +import attr @pytest.fixture(name="mp") def _mp(): - return metadata_proxy({"x": 42, "y": "foo"}) + return types.MappingProxyType({"x": 42, "y": "foo"}) class TestMetadataProxy: """ - Ensure properties of metadata_proxy independently of hypothesis strategies. + Ensure properties of metadata proxy independently of hypothesis strategies. """ def test_repr(self, mp): @@ -50,3 +52,13 @@ class TestMetadataProxy: with pytest.raises(AttributeError, match="no attribute 'setdefault'"): mp.setdefault("x") + + +def test_attrsinstance_subclass_protocol(): + """ + It's possible to subclass AttrsInstance and Protocol at once. + """ + + class Foo(attr.AttrsInstance, attr._compat.Protocol): + def attribute(self) -> int: + ... diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_config.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_config.py index bbf67564064..6c78fd295b5 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_config.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_config.py @@ -4,14 +4,13 @@ Tests for `attr._config`. """ -from __future__ import absolute_import, division, print_function import pytest from attr import _config -class TestConfig(object): +class TestConfig: def test_default(self): """ Run validators by default. diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_converters.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_converters.py index d0fc723eb1b..7607e555066 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_converters.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_converters.py @@ -4,7 +4,6 @@ Tests for `attr.converters`. """ -from __future__ import absolute_import import pytest @@ -14,7 +13,7 @@ from attr import Factory, attrib from attr.converters import default_if_none, optional, pipe, to_bool -class TestOptional(object): +class TestOptional: """ Tests for `optional`. """ @@ -45,7 +44,7 @@ class TestOptional(object): c("not_an_int") -class TestDefaultIfNone(object): +class TestDefaultIfNone: def test_missing_default(self): """ Raises TypeError if neither default nor factory have been passed. @@ -101,7 +100,7 @@ class TestDefaultIfNone(object): assert [] == c(None) -class TestPipe(object): +class TestPipe: def test_success(self): """ Succeeds if all wrapped converters succeed. @@ -130,15 +129,23 @@ class TestPipe(object): """ @attr.s - class C(object): + class C: a1 = attrib(default="True", converter=pipe(str, to_bool, bool)) a2 = attrib(default=True, converter=[str, to_bool, bool]) c = C() assert True is c.a1 is c.a2 + def test_empty(self): + """ + Empty pipe returns same value. + """ + o = object() + + assert o is pipe()(o) + -class TestToBool(object): +class TestToBool: def test_unhashable(self): """ Fails if value is unhashable. diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_dunders.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_dunders.py index 186762eb0da..d0d289d84c9 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_dunders.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_dunders.py @@ -4,9 +4,9 @@ Tests for dunder methods from `attrib._make`. """ -from __future__ import absolute_import, division, print_function import copy +import inspect import pickle import pytest @@ -22,7 +22,6 @@ from attr._make import ( _add_repr, _is_slot_cls, _make_init, - _Nothing, fields, make_class, ) @@ -40,25 +39,25 @@ ReprCSlots = simple_class(repr=True, slots=True) @attr.s(eq=True) -class EqCallableC(object): +class EqCallableC: a = attr.ib(eq=str.lower, order=False) b = attr.ib(eq=True) @attr.s(eq=True, slots=True) -class EqCallableCSlots(object): +class EqCallableCSlots: a = attr.ib(eq=str.lower, order=False) b = attr.ib(eq=True) @attr.s(order=True) -class OrderCallableC(object): +class OrderCallableC: a = attr.ib(eq=True, order=str.lower) b = attr.ib(order=True) @attr.s(order=True, slots=True) -class OrderCallableCSlots(object): +class OrderCallableCSlots: a = attr.ib(eq=True, order=str.lower) b = attr.ib(order=True) @@ -86,10 +85,15 @@ def _add_init(cls, frozen): This function used to be part of _make. It wasn't used anymore however the tests for it are still useful to test the behavior of _make_init. """ + has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + cls.__init__ = _make_init( cls, cls.__attrs_attrs__, - getattr(cls, "__attrs_pre_init__", False), + has_pre_init, + len(inspect.signature(cls.__attrs_pre_init__).parameters) > 1 + if has_pre_init + else False, getattr(cls, "__attrs_post_init__", False), frozen, _is_slot_cls(cls), @@ -102,14 +106,14 @@ def _add_init(cls, frozen): return cls -class InitC(object): +class InitC: __attrs_attrs__ = [simple_attr("a"), simple_attr("b")] InitC = _add_init(InitC, False) -class TestEqOrder(object): +class TestEqOrder: """ Tests for eq and order related methods. """ @@ -168,7 +172,7 @@ class TestEqOrder(object): match. """ - class NotEqC(object): + class NotEqC: a = 1 b = 2 @@ -316,12 +320,11 @@ class TestEqOrder(object): assert NotImplemented == (cls(1, 2).__ge__(42)) -class TestAddRepr(object): +class TestAddRepr: """ Tests for `_add_repr`. """ - @pytest.mark.parametrize("slots", [True, False]) def test_repr(self, slots): """ If `repr` is False, ignore that attribute. @@ -349,7 +352,7 @@ class TestAddRepr(object): return "foo:" + str(value) @attr.s - class C(object): + class C: a = attr.ib(repr=custom_repr) assert "C(a=foo:1)" == repr(C(1)) @@ -361,7 +364,7 @@ class TestAddRepr(object): """ @attr.s - class Cycle(object): + class Cycle: value = attr.ib(default=7) cycle = attr.ib(default=None) @@ -376,7 +379,7 @@ class TestAddRepr(object): """ @attr.s - class LongCycle(object): + class LongCycle: value = attr.ib(default=14) cycle = attr.ib(default=None) @@ -391,7 +394,7 @@ class TestAddRepr(object): repr does not strip underscores. """ - class C(object): + class C: __attrs_attrs__ = [simple_attr("_x")] C = _add_repr(C) @@ -440,21 +443,21 @@ class TestAddRepr(object): # these are for use in TestAddHash.test_cache_hash_serialization # they need to be out here so they can be un-pickled @attr.attrs(hash=True, cache_hash=False) -class HashCacheSerializationTestUncached(object): +class HashCacheSerializationTestUncached: foo_value = attr.ib() @attr.attrs(hash=True, cache_hash=True) -class HashCacheSerializationTestCached(object): +class HashCacheSerializationTestCached: foo_value = attr.ib() @attr.attrs(slots=True, hash=True, cache_hash=True) -class HashCacheSerializationTestCachedSlots(object): +class HashCacheSerializationTestCachedSlots: foo_value = attr.ib() -class IncrementingHasher(object): +class IncrementingHasher: def __init__(self): self.hash_value = 100 @@ -464,7 +467,7 @@ class IncrementingHasher(object): return rv -class TestAddHash(object): +class TestAddHash: """ Tests for `_add_hash`. """ @@ -647,21 +650,19 @@ class TestAddHash(object): assert 1 == cached_instance.hash_counter.times_hash_called @pytest.mark.parametrize("cache_hash", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) - @pytest.mark.parametrize("slots", [True, False]) def test_copy_hash_cleared(self, cache_hash, frozen, slots): """ Test that the default hash is recalculated after a copy operation. """ - kwargs = dict(frozen=frozen, slots=slots, cache_hash=cache_hash) + kwargs = {"frozen": frozen, "slots": slots, "cache_hash": cache_hash} # Give it an explicit hash if we don't have an implicit one if not frozen: kwargs["hash"] = True @attr.s(**kwargs) - class C(object): + class C: x = attr.ib() a = C(IncrementingHasher()) @@ -676,7 +677,7 @@ class TestAddHash(object): assert orig_hash != hash(b) @pytest.mark.parametrize( - "klass,cached", + ("klass", "cached"), [ (HashCacheSerializationTestUncached, False), (HashCacheSerializationTestCached, True), @@ -702,7 +703,6 @@ class TestAddHash(object): assert original_hash != hash(obj_rt) - @pytest.mark.parametrize("frozen", [True, False]) def test_copy_two_arg_reduce(self, frozen): """ If __getstate__ returns None, the tuple returned by object.__reduce__ @@ -711,7 +711,7 @@ class TestAddHash(object): """ @attr.s(frozen=frozen, cache_hash=True, hash=True) - class C(object): + class C: x = attr.ib() def __getstate__(self): @@ -729,7 +729,7 @@ class TestAddHash(object): return pickle.loads(pickle_str) -class TestAddInit(object): +class TestAddInit: """ Tests for `_add_init`. """ @@ -802,7 +802,7 @@ class TestAddInit(object): If a default value is present, it's used as fallback. """ - class C(object): + class C: __attrs_attrs__ = [ simple_attr(name="a", default=2), simple_attr(name="b", default="hallo"), @@ -820,10 +820,10 @@ class TestAddInit(object): If a default factory is present, it's used as fallback. """ - class D(object): + class D: pass - class C(object): + class C: __attrs_attrs__ = [ simple_attr(name="a", default=Factory(list)), simple_attr(name="b", default=Factory(D)), @@ -898,7 +898,7 @@ class TestAddInit(object): underscores. """ - class C(object): + class C: __attrs_attrs__ = [simple_attr("_private")] C = _add_init(C, False) @@ -906,32 +906,32 @@ class TestAddInit(object): assert 42 == i._private -class TestNothing(object): +class TestNothing: """ - Tests for `_Nothing`. + Tests for `NOTHING`. """ def test_copy(self): """ __copy__ returns the same object. """ - n = _Nothing() + n = NOTHING assert n is copy.copy(n) def test_deepcopy(self): """ __deepcopy__ returns the same object. """ - n = _Nothing() + n = NOTHING assert n is copy.deepcopy(n) def test_eq(self): """ All instances are equal. """ - assert _Nothing() == _Nothing() == NOTHING - assert not (_Nothing() != _Nothing()) - assert 1 != _Nothing() + assert NOTHING == NOTHING == NOTHING + assert not (NOTHING != NOTHING) + assert 1 != NOTHING def test_false(self): """ @@ -942,7 +942,7 @@ class TestNothing(object): @attr.s(hash=True, order=True) -class C(object): +class C: pass @@ -951,7 +951,7 @@ OriginalC = C @attr.s(hash=True, order=True) -class C(object): +class C: pass @@ -959,13 +959,13 @@ CopyC = C @attr.s(hash=True, order=True) -class C(object): +class C: """A different class, to generate different methods.""" a = attr.ib() -class TestFilenames(object): +class TestFilenames: def test_filenames(self): """ The created dunder methods have a "consistent" filename. diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_filters.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_filters.py index d1ec24dc6c2..6d237fdc3d1 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_filters.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_filters.py @@ -4,7 +4,6 @@ Tests for `attr.filters`. """ -from __future__ import absolute_import, division, print_function import pytest @@ -15,12 +14,12 @@ from attr.filters import _split_what, exclude, include @attr.s -class C(object): +class C: a = attr.ib() b = attr.ib() -class TestSplitWhat(object): +class TestSplitWhat: """ Tests for `_split_what`. """ @@ -31,22 +30,27 @@ class TestSplitWhat(object): """ assert ( frozenset((int, str)), + frozenset(("abcd", "123")), frozenset((fields(C).a,)), - ) == _split_what((str, fields(C).a, int)) + ) == _split_what((str, "123", fields(C).a, int, "abcd")) -class TestInclude(object): +class TestInclude: """ Tests for `include`. """ @pytest.mark.parametrize( - "incl,value", + ("incl", "value"), [ ((int,), 42), ((str,), "hello"), ((str, fields(C).a), 42), ((str, fields(C).b), "hello"), + (("a",), 42), + (("a",), "hello"), + (("a", str), 42), + (("a", fields(C).b), "hello"), ], ) def test_allow(self, incl, value): @@ -57,12 +61,16 @@ class TestInclude(object): assert i(fields(C).a, value) is True @pytest.mark.parametrize( - "incl,value", + ("incl", "value"), [ ((str,), 42), ((int,), "hello"), ((str, fields(C).b), 42), ((int, fields(C).b), "hello"), + (("b",), 42), + (("b",), "hello"), + (("b", str), 42), + (("b", fields(C).b), "hello"), ], ) def test_drop_class(self, incl, value): @@ -73,18 +81,22 @@ class TestInclude(object): assert i(fields(C).a, value) is False -class TestExclude(object): +class TestExclude: """ Tests for `exclude`. """ @pytest.mark.parametrize( - "excl,value", + ("excl", "value"), [ ((str,), 42), ((int,), "hello"), ((str, fields(C).b), 42), ((int, fields(C).b), "hello"), + (("b",), 42), + (("b",), "hello"), + (("b", str), 42), + (("b", fields(C).b), "hello"), ], ) def test_allow(self, excl, value): @@ -95,12 +107,16 @@ class TestExclude(object): assert e(fields(C).a, value) is True @pytest.mark.parametrize( - "excl,value", + ("excl", "value"), [ ((int,), 42), ((str,), "hello"), ((str, fields(C).a), 42), ((str, fields(C).b), "hello"), + (("a",), 42), + (("a",), "hello"), + (("a", str), 42), + (("a", fields(C).b), "hello"), ], ) def test_drop_class(self, excl, value): diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_funcs.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_funcs.py index 4490ed815ae..044aaab2c94 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_funcs.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_funcs.py @@ -4,9 +4,10 @@ Tests for `attr._funcs`. """ -from __future__ import absolute_import, division, print_function +import re from collections import OrderedDict +from typing import Generic, NamedTuple, TypeVar import pytest @@ -16,7 +17,7 @@ from hypothesis import strategies as st import attr from attr import asdict, assoc, astuple, evolve, fields, has -from attr._compat import TYPE, Mapping, Sequence, ordered_dict +from attr._compat import Mapping, Sequence from attr.exceptions import AttrsAttributeNotFoundError from attr.validators import instance_of @@ -35,14 +36,14 @@ def _C(): import attr @attr.s - class C(object): + class C: x = attr.ib() y = attr.ib() return C -class TestAsDict(object): +class TestAsDict: """ Tests for `asdict`. """ @@ -197,7 +198,7 @@ class TestAsDict(object): Field order should be preserved when dumping to an ordered_dict. """ instance = cls() - dict_instance = asdict(instance, dict_factory=ordered_dict) + dict_instance = asdict(instance, dict_factory=dict) assert [a.name for a in fields(cls)] == list(dict_instance.keys()) @@ -207,7 +208,7 @@ class TestAsDict(object): """ @attr.s - class A(object): + class A: a = attr.ib() instance = A({(1,): 1}) @@ -225,15 +226,61 @@ class TestAsDict(object): """ @attr.s - class A(object): + class A: a = attr.ib() instance = A({(1,): 1}) assert {"a": {(1,): 1}} == attr.asdict(instance) + def test_named_tuple_retain_type(self): + """ + Namedtuples can be serialized if retain_collection_types is True. + + See #1164 + """ + + class Coordinates(NamedTuple): + lat: float + lon: float + + @attr.s + class A: + coords: Coordinates = attr.ib() + + instance = A(Coordinates(50.419019, 30.516225)) + + assert {"coords": Coordinates(50.419019, 30.516225)} == attr.asdict( + instance, retain_collection_types=True + ) + + def test_type_error_with_retain_type(self): + """ + Serialization that fails with TypeError leaves the error through if + they're not tuples. + + See #1164 + """ -class TestAsTuple(object): + message = "__new__() missing 1 required positional argument (asdict)" + + class Coordinates(list): + def __init__(self, first, *rest): + if isinstance(first, list): + raise TypeError(message) + super().__init__([first, *rest]) + + @attr.s + class A: + coords: Coordinates = attr.ib() + + instance = A(Coordinates(50.419019, 30.516225)) + + with pytest.raises(TypeError, match=re.escape(message)): + attr.asdict(instance, retain_collection_types=True) + + +class TestAsTuple: """ Tests for `astuple`. """ @@ -390,8 +437,54 @@ class TestAsTuple(object): assert (1, [1, 2, 3]) == d + def test_named_tuple_retain_type(self): + """ + Namedtuples can be serialized if retain_collection_types is True. -class TestHas(object): + See #1164 + """ + + class Coordinates(NamedTuple): + lat: float + lon: float + + @attr.s + class A: + coords: Coordinates = attr.ib() + + instance = A(Coordinates(50.419019, 30.516225)) + + assert (Coordinates(50.419019, 30.516225),) == attr.astuple( + instance, retain_collection_types=True + ) + + def test_type_error_with_retain_type(self): + """ + Serialization that fails with TypeError leaves the error through if + they're not tuples. + + See #1164 + """ + + message = "__new__() missing 1 required positional argument (astuple)" + + class Coordinates(list): + def __init__(self, first, *rest): + if isinstance(first, list): + raise TypeError(message) + super().__init__([first, *rest]) + + @attr.s + class A: + coords: Coordinates = attr.ib() + + instance = A(Coordinates(50.419019, 30.516225)) + + with pytest.raises(TypeError, match=re.escape(message)): + attr.astuple(instance, retain_collection_types=True) + + +class TestHas: """ Tests for `has`. """ @@ -408,7 +501,7 @@ class TestHas(object): """ @attr.s - class D(object): + class D: pass assert has(D) @@ -419,8 +512,39 @@ class TestHas(object): """ assert not has(object) + def test_generics(self): + """ + Works with generic classes. + """ + T = TypeVar("T") + + @attr.define + class A(Generic[T]): + a: T + + assert has(A) + + assert has(A[str]) + # Verify twice, since there's caching going on. + assert has(A[str]) + + def test_generics_negative(self): + """ + Returns `False` on non-decorated generic classes. + """ + T = TypeVar("T") + + class A(Generic[T]): + a: T + + assert not has(A) + + assert not has(A[str]) + # Verify twice, since there's caching going on. + assert not has(A[str]) + -class TestAssoc(object): +class TestAssoc: """ Tests for `assoc`. """ @@ -432,12 +556,11 @@ class TestAssoc(object): """ @attr.s(slots=slots, frozen=frozen) - class C(object): + class C: pass i1 = C() - with pytest.deprecated_call(): - i2 = assoc(i1) + i2 = assoc(i1) assert i1 is not i2 assert i1 == i2 @@ -448,8 +571,7 @@ class TestAssoc(object): No changes means a verbatim copy. """ i1 = C() - with pytest.deprecated_call(): - i2 = assoc(i1) + i2 = assoc(i1) assert i1 is not i2 assert i1 == i2 @@ -466,8 +588,7 @@ class TestAssoc(object): chosen_names = data.draw(st.sets(st.sampled_from(field_names))) change_dict = {name: data.draw(st.integers()) for name in chosen_names} - with pytest.deprecated_call(): - changed = assoc(original, **change_dict) + changed = assoc(original, **change_dict) for k, v in change_dict.items(): assert getattr(changed, k) == v @@ -484,9 +605,7 @@ class TestAssoc(object): ) as e, pytest.deprecated_call(): assoc(C(), aaaa=2) - assert ( - "aaaa is not an attrs attribute on {cls!r}.".format(cls=C), - ) == e.value.args + assert (f"aaaa is not an attrs attribute on {C!r}.",) == e.value.args def test_frozen(self): """ @@ -494,29 +613,14 @@ class TestAssoc(object): """ @attr.s(frozen=True) - class C(object): + class C: x = attr.ib() y = attr.ib() - with pytest.deprecated_call(): - assert C(3, 2) == assoc(C(1, 2), x=3) + assert C(3, 2) == assoc(C(1, 2), x=3) - def test_warning(self): - """ - DeprecationWarning points to the correct file. - """ - - @attr.s - class C(object): - x = attr.ib() - with pytest.warns(DeprecationWarning) as wi: - assert C(2) == assoc(C(1), x=2) - - assert __file__ == wi.list[0].filename - - -class TestEvolve(object): +class TestEvolve: """ Tests for `evolve`. """ @@ -528,7 +632,7 @@ class TestEvolve(object): """ @attr.s(slots=slots, frozen=frozen) - class C(object): + class C: pass i1 = C() @@ -593,14 +697,14 @@ class TestEvolve(object): """ @attr.s - class C(object): + class C: a = attr.ib(validator=instance_of(int)) with pytest.raises(TypeError) as e: evolve(C(a=1), a="some string") m = e.value.args[0] - assert m.startswith("'a' must be <{type} 'int'>".format(type=TYPE)) + assert m.startswith("'a' must be <class 'int'>") def test_private(self): """ @@ -608,7 +712,7 @@ class TestEvolve(object): """ @attr.s - class C(object): + class C: _a = attr.ib() assert evolve(C(1), a=2)._a == 2 @@ -625,7 +729,7 @@ class TestEvolve(object): """ @attr.s - class C(object): + class C: a = attr.ib() b = attr.ib(init=False, default=0) @@ -639,11 +743,11 @@ class TestEvolve(object): """ @attr.s - class Cls1(object): + class Cls1: param1 = attr.ib() @attr.s - class Cls2(object): + class Cls2: param2 = attr.ib() obj2a = Cls2(param2="a") @@ -663,11 +767,11 @@ class TestEvolve(object): """ @attr.s - class Cls1(object): + class Cls1: param1 = attr.ib() @attr.s - class Cls2(object): + class Cls2: param2 = attr.ib() obj2a = Cls2(param2="a") @@ -678,3 +782,47 @@ class TestEvolve(object): assert Cls1({"foo": 42, "param2": 42}) == attr.evolve( obj1a, param1=obj2b ) + + def test_inst_kw(self): + """ + If `inst` is passed per kw argument, a warning is raised. + See #1109 + """ + + @attr.s + class C: + pass + + with pytest.warns(DeprecationWarning) as wi: + evolve(inst=C()) + + assert __file__ == wi.list[0].filename + + def test_no_inst(self): + """ + Missing inst argument raises a TypeError like Python would. + """ + with pytest.raises(TypeError, match=r"evolve\(\) missing 1"): + evolve(x=1) + + def test_too_many_pos_args(self): + """ + More than one positional argument raises a TypeError like Python would. + """ + with pytest.raises( + TypeError, + match=r"evolve\(\) takes 1 positional argument, but 2 were given", + ): + evolve(1, 2) + + def test_can_change_inst(self): + """ + If the instance is passed by positional argument, a field named `inst` + can be changed. + """ + + @attr.define + class C: + inst: int + + assert C(42) == evolve(C(23), inst=42) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_functional.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_functional.py index 9b6a27e2f4d..341ee50a82a 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_functional.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_functional.py @@ -4,7 +4,6 @@ End-to-end tests. """ -from __future__ import absolute_import, division, print_function import inspect import pickle @@ -12,28 +11,24 @@ import pickle from copy import deepcopy import pytest -import six -from hypothesis import assume, given +from hypothesis import given from hypothesis.strategies import booleans import attr -from attr._compat import PY2, PY36, TYPE from attr._make import NOTHING, Attribute from attr.exceptions import FrozenInstanceError -from .strategies import optional_bool - @attr.s -class C1(object): +class C1: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @attr.s(slots=True) -class C1Slots(object): +class C1Slots: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @@ -42,19 +37,19 @@ foo = None @attr.s() -class C2(object): +class C2: x = attr.ib(default=foo) y = attr.ib(default=attr.Factory(list)) @attr.s(slots=True) -class C2Slots(object): +class C2Slots: x = attr.ib(default=foo) y = attr.ib(default=attr.Factory(list)) @attr.s -class Base(object): +class Base: x = attr.ib() def meth(self): @@ -62,7 +57,7 @@ class Base(object): @attr.s(slots=True) -class BaseSlots(object): +class BaseSlots: x = attr.ib() def meth(self): @@ -80,7 +75,7 @@ class SubSlots(BaseSlots): @attr.s(frozen=True, slots=True) -class Frozen(object): +class Frozen: x = attr.ib() @@ -90,7 +85,7 @@ class SubFrozen(Frozen): @attr.s(frozen=True, slots=False) -class FrozenNoSlots(object): +class FrozenNoSlots: x = attr.ib() @@ -99,21 +94,19 @@ class Meta(type): @attr.s -@six.add_metaclass(Meta) -class WithMeta(object): +class WithMeta(metaclass=Meta): pass @attr.s(slots=True) -@six.add_metaclass(Meta) -class WithMetaSlots(object): +class WithMetaSlots(metaclass=Meta): pass FromMakeClass = attr.make_class("FromMakeClass", ["x"]) -class TestFunctional(object): +class TestFunctional: """ Functional tests. """ @@ -126,6 +119,7 @@ class TestFunctional(object): assert ( Attribute( name="x", + alias="x", default=foo, validator=None, repr=True, @@ -138,6 +132,7 @@ class TestFunctional(object): ), Attribute( name="y", + alias="y", default=attr.Factory(list), validator=None, repr=True, @@ -167,8 +162,7 @@ class TestFunctional(object): # Using C1 explicitly, since slotted classes don't support this. assert ( - "'x' must be <{type} 'int'> (got '1' that is a <{type} " - "'str'>).".format(type=TYPE), + "'x' must be <class 'int'> (got '1' that is a <class 'str'>).", attr.fields(C1).x, int, "1", @@ -181,7 +175,7 @@ class TestFunctional(object): """ @attr.s(slots=slots) - class C3(object): + class C3: _x = attr.ib() assert "C3(_x=1)" == repr(C3(x=1)) @@ -196,6 +190,7 @@ class TestFunctional(object): assert ( Attribute( name="a", + alias="a", default=NOTHING, validator=None, repr=True, @@ -208,6 +203,7 @@ class TestFunctional(object): ), Attribute( name="b", + alias="b", default=NOTHING, validator=None, repr=True, @@ -231,9 +227,9 @@ class TestFunctional(object): assert i.x is i.meth() is obj assert i.y == 2 if cls is Sub: - assert "Sub(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"Sub(x={obj}, y=2)" == repr(i) else: - assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i) + assert f"SubSlots(x={obj}, y=2)" == repr(i) @pytest.mark.parametrize("base", [Base, BaseSlots]) def test_subclass_without_extra_attrs(self, base): @@ -248,7 +244,7 @@ class TestFunctional(object): obj = object() i = Sub2(x=obj) assert i.x is i.meth() is obj - assert "Sub2(x={obj})".format(obj=obj) == repr(i) + assert f"Sub2(x={obj})" == repr(i) @pytest.mark.parametrize( "frozen_class", @@ -317,10 +313,7 @@ class TestFunctional(object): """ Pickle object serialization works on all kinds of attrs classes. """ - if len(attr.fields(cls)) == 2: - obj = cls(123, 456) - else: - obj = cls(123) + obj = cls(123, 456) if len(attr.fields(cls)) == 2 else cls(123) assert repr(obj) == repr(pickle.loads(pickle.dumps(obj, protocol))) @@ -351,7 +344,7 @@ class TestFunctional(object): """ @attr.s - class C(object): + class C: x = attr.ib(default=1) y = attr.ib() @@ -361,8 +354,6 @@ class TestFunctional(object): assert C(1, 2) == C() - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) @pytest.mark.parametrize("weakref_slot", [True, False]) def test_attrib_overwrite(self, slots, frozen, weakref_slot): """ @@ -380,7 +371,7 @@ class TestFunctional(object): dict-classes are never replaced. """ - class C(object): + class C: x = attr.ib() C_new = attr.s(C) @@ -395,7 +386,7 @@ class TestFunctional(object): """ @attr.s(hash=False) - class HashByIDBackwardCompat(object): + class HashByIDBackwardCompat: x = attr.ib() assert hash(HashByIDBackwardCompat(1)) != hash( @@ -403,13 +394,13 @@ class TestFunctional(object): ) @attr.s(hash=False, eq=False) - class HashByID(object): + class HashByID: x = attr.ib() assert hash(HashByID(1)) != hash(HashByID(1)) @attr.s(hash=True) - class HashByValues(object): + class HashByValues: x = attr.ib() assert hash(HashByValues(1)) == hash(HashByValues(1)) @@ -420,37 +411,35 @@ class TestFunctional(object): """ @attr.s - class Unhashable(object): + class Unhashable: pass @attr.s - class C(object): + class C: x = attr.ib(default=Unhashable()) @attr.s class D(C): pass - @pytest.mark.parametrize("slots", [True, False]) def test_hash_false_eq_false(self, slots): """ hash=False and eq=False make a class hashable by ID. """ @attr.s(hash=False, eq=False, slots=slots) - class C(object): + class C: pass assert hash(C()) != hash(C()) - @pytest.mark.parametrize("slots", [True, False]) def test_eq_false(self, slots): """ eq=False makes a class hashable by ID. """ @attr.s(eq=False, slots=slots) - class C(object): + class C: pass # Ensure both objects live long enough such that their ids/hashes @@ -468,7 +457,7 @@ class TestFunctional(object): """ @attr.s - class C(object): + class C: c = attr.ib(default=100) x = attr.ib(default=1) b = attr.ib(default=23) @@ -515,7 +504,7 @@ class TestFunctional(object): slots=base_slots, weakref_slot=base_weakref_slot, ) - class Base(object): + class Base: a = attr.ib(converter=int if base_converter else None) @attr.s( @@ -542,7 +531,7 @@ class TestFunctional(object): """ @attr.s - class C(object): + class C: property = attr.ib() itemgetter = attr.ib() x = attr.ib() @@ -551,8 +540,6 @@ class TestFunctional(object): assert "itemgetter" == attr.fields(C).itemgetter.name assert "x" == attr.fields(C).x.name - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_auto_exc(self, slots, frozen): """ Classes with auto_exc=True have a Exception-style __str__, compare and @@ -607,8 +594,6 @@ class TestFunctional(object): deepcopy(e1) deepcopy(e2) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_auto_exc_one_attrib(self, slots, frozen): """ Having one attribute works with auto_exc=True. @@ -622,76 +607,39 @@ class TestFunctional(object): FooError(1) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_eq_only(self, slots, frozen): """ Classes with order=False cannot be ordered. - - Python 3 throws a TypeError, in Python2 we have to check for the - absence. """ @attr.s(eq=True, order=False, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() - if not PY2: - possible_errors = ( - "unorderable types: C() < C()", - "'<' not supported between instances of 'C' and 'C'", - "unorderable types: C < C", # old PyPy 3 - ) + possible_errors = ( + "unorderable types: C() < C()", + "'<' not supported between instances of 'C' and 'C'", + "unorderable types: C < C", # old PyPy 3 + ) - with pytest.raises(TypeError) as ei: - C(5) < C(6) + with pytest.raises(TypeError) as ei: + C(5) < C(6) - assert ei.value.args[0] in possible_errors - else: - i = C(42) - for m in ("lt", "le", "gt", "ge"): - assert None is getattr(i, "__%s__" % (m,), None) - - @given(cmp=optional_bool, eq=optional_bool, order=optional_bool) - def test_cmp_deprecated_attribute(self, cmp, eq, order): - """ - Accessing Attribute.cmp raises a deprecation warning but returns True - if cmp is True, or eq and order are *both* effectively True. - """ - # These cases are invalid and raise a ValueError. - assume(cmp is None or (eq is None and order is None)) - assume(not (eq is False and order is True)) - - if cmp is not None: - rv = cmp - elif eq is True or eq is None: - rv = order is None or order is True - elif cmp is None and eq is None and order is None: - rv = True - elif cmp is None or eq is None: - rv = False - else: - pytest.fail( - "Unexpected state: cmp=%r eq=%r order=%r" % (cmp, eq, order) - ) + assert ei.value.args[0] in possible_errors - with pytest.deprecated_call() as dc: - - @attr.s - class C(object): - x = attr.ib(cmp=cmp, eq=eq, order=order) + @pytest.mark.parametrize("cmp", [True, False]) + def test_attrib_cmp_shortcut(self, slots, cmp): + """ + Setting cmp on `attr.ib`s sets both eq and order. + """ - assert rv == attr.fields(C).x.cmp + @attr.s(slots=slots) + class C: + x = attr.ib(cmp=cmp) - (w,) = dc.list + assert cmp is attr.fields(C).x.eq + assert cmp is attr.fields(C).x.order - assert ( - "The usage of `cmp` is deprecated and will be removed on or after " - "2021-06-01. Please use `eq` and `order` instead." - == w.message.args[0] - ) - - @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_if_validate_without_validators(self, slots): """ If a class has on_setattr=attr.setters.validate (former default in NG @@ -701,11 +649,11 @@ class TestFunctional(object): Regression test for #816. """ - @attr.s(on_setattr=attr.setters.validate) - class C(object): + @attr.s(on_setattr=attr.setters.validate, slots=slots) + class C: x = attr.ib() - @attr.s(on_setattr=attr.setters.validate) + @attr.s(on_setattr=attr.setters.validate, slots=slots) class D(C): y = attr.ib() @@ -716,18 +664,17 @@ class TestFunctional(object): assert "self.y = y" in src assert object.__setattr__ == D.__setattr__ - @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_if_convert_without_converters(self, slots): """ If a class has on_setattr=attr.setters.convert but sets no validators, don't use the (slower) setattr in __init__. """ - @attr.s(on_setattr=attr.setters.convert) - class C(object): + @attr.s(on_setattr=attr.setters.convert, slots=slots) + class C: x = attr.ib() - @attr.s(on_setattr=attr.setters.convert) + @attr.s(on_setattr=attr.setters.convert, slots=slots) class D(C): y = attr.ib() @@ -738,8 +685,6 @@ class TestFunctional(object): assert "self.y = y" in src assert object.__setattr__ == D.__setattr__ - @pytest.mark.skipif(not PY36, reason="NG APIs are 3.6+") - @pytest.mark.parametrize("slots", [True, False]) def test_no_setattr_with_ng_defaults(self, slots): """ If a class has the NG default on_setattr=[convert, validate] but sets @@ -747,8 +692,8 @@ class TestFunctional(object): __init__. """ - @attr.define - class C(object): + @attr.define(slots=slots) + class C: x = attr.ib() src = inspect.getsource(C.__init__) @@ -757,7 +702,7 @@ class TestFunctional(object): assert "self.x = x" in src assert object.__setattr__ == C.__setattr__ - @attr.define + @attr.define(slots=slots) class D(C): y = attr.ib() @@ -775,7 +720,7 @@ class TestFunctional(object): """ @attr.s(on_setattr=attr.setters.validate) - class C(object): + class C: x = attr.ib(validator=42) @attr.s(on_setattr=attr.setters.validate) @@ -784,7 +729,18 @@ class TestFunctional(object): src = inspect.getsource(D.__init__) - assert "_setattr = _cached_setattr" in src + assert "_setattr = _cached_setattr_get(self)" in src assert "_setattr('x', x)" in src assert "_setattr('y', y)" in src assert object.__setattr__ != D.__setattr__ + + def test_unsafe_hash(self, slots): + """ + attr.s(unsafe_hash=True) makes a class hashable. + """ + + @attr.s(slots=slots, unsafe_hash=True) + class Hashable: + pass + + assert hash(Hashable()) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_hooks.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_hooks.py index 92fc2dcaab5..9c37a98cdc0 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_hooks.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_hooks.py @@ -1,7 +1,8 @@ # SPDX-License-Identifier: MIT +from __future__ import annotations + from datetime import datetime -from typing import Dict, List import attr @@ -99,6 +100,24 @@ class TestTransformHook: assert attr.asdict(C(1, 2)) == {"x": 1, "new": 2} + def test_hook_override_alias(self): + """ + It is possible to set field alias via hook + """ + + def use_dataclass_names(cls, attribs): + return [a.evolve(alias=a.name) for a in attribs] + + @attr.s(auto_attribs=True, field_transformer=use_dataclass_names) + class NameCase: + public: int + _private: int + __dunder__: int + + assert NameCase(public=1, _private=2, __dunder__=3) == NameCase( + 1, 2, 3 + ) + def test_hook_with_inheritance(self): """ The hook receives all fields from base classes. @@ -151,14 +170,14 @@ class TestAsDictHook: @attr.dataclass class Child: x: datetime - y: List[datetime] + y: list[datetime] @attr.dataclass class Parent: a: Child - b: List[Child] - c: Dict[str, Child] - d: Dict[str, datetime] + b: list[Child] + c: dict[str, Child] + d: dict[str, datetime] inst = Parent( a=Child(1, [datetime(2020, 7, 1)]), @@ -192,8 +211,8 @@ class TestAsDictHook: @attr.dataclass class Parent: a: Child - b: List[Child] - c: Dict[str, Child] + b: list[Child] + c: dict[str, Child] inst = Parent(a=Child(1), b=[Child(2)], c={"spam": Child(3)}) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_import.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_import.py index 423124319c9..9e90a5c11e6 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_import.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_import.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT -class TestImportStar(object): +class TestImportStar: def test_from_attr_import_star(self): """ import * from attr diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_init_subclass.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_init_subclass.py index 863e794377d..cff4e948bcb 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_init_subclass.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_init_subclass.py @@ -1,17 +1,12 @@ # SPDX-License-Identifier: MIT """ -Tests for `__init_subclass__` related tests. - -Python 3.6+ only. +Tests for `__init_subclass__` related functionality. """ -import pytest - import attr -@pytest.mark.parametrize("slots", [True, False]) def test_init_subclass_vanilla(slots): """ `super().__init_subclass__` can be used if the subclass is not an attrs @@ -46,3 +41,26 @@ def test_init_subclass_attrs(): pass assert "foo" == Attrs().param + + +def test_init_subclass_slots_workaround(): + """ + `__init_subclass__` works with modern APIs if care is taken around classes + existing twice. + """ + subs = {} + + @attr.define + class Base: + def __init_subclass__(cls): + subs[cls.__qualname__] = cls + + @attr.define + class Sub1(Base): + x: int + + @attr.define + class Sub2(Base): + y: int + + assert (Sub1, Sub2) == tuple(subs.values()) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_make.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_make.py index 729d3a71f06..19f7a4cd412 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_make.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_make.py @@ -4,7 +4,6 @@ Tests for `attr._make`. """ -from __future__ import absolute_import, division, print_function import copy import functools @@ -14,6 +13,7 @@ import itertools import sys from operator import attrgetter +from typing import Generic, TypeVar import pytest @@ -23,7 +23,7 @@ from hypothesis.strategies import booleans, integers, lists, sampled_from, text import attr from attr import _config -from attr._compat import PY2, PY310, ordered_dict +from attr._compat import PY310 from attr._make import ( Attribute, Factory, @@ -41,11 +41,7 @@ from attr._make import ( make_class, validate, ) -from attr.exceptions import ( - DefaultAlreadySetError, - NotAnAttrsClassError, - PythonTooOldError, -) +from attr.exceptions import DefaultAlreadySetError, NotAnAttrsClassError from .strategies import ( gen_attr_names, @@ -62,7 +58,20 @@ from .utils import simple_attr attrs_st = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c)) -class TestCountingAttr(object): +@pytest.fixture(name="with_and_without_validation", params=[True, False]) +def _with_and_without_validation(request): + """ + Run tests with and without validation enabled. + """ + attr.validators.set_disabled(request.param) + + try: + yield + finally: + attr.validators.set_disabled(False) + + +class TestCountingAttr: """ Tests for `attr`. """ @@ -151,7 +160,7 @@ class TestCountingAttr(object): def make_tc(): - class TransformC(object): + class TransformC: z = attr.ib() y = attr.ib() x = attr.ib() @@ -160,7 +169,7 @@ def make_tc(): return TransformC -class TestTransformAttrs(object): +class TestTransformAttrs: """ Tests for `_transform_attrs`. """ @@ -189,7 +198,7 @@ class TestTransformAttrs(object): """ @attr.s - class C(object): + class C: pass assert _Attributes(((), [], {})) == _transform_attrs( @@ -215,7 +224,7 @@ class TestTransformAttrs(object): mandatory attributes. """ - class C(object): + class C: x = attr.ib(default=None) y = attr.ib() @@ -228,20 +237,20 @@ class TestTransformAttrs(object): "eq=True, eq_key=None, order=True, order_key=None, " "hash=None, init=True, " "metadata=mappingproxy({}), type=None, converter=None, " - "kw_only=False, inherited=False, on_setattr=None)", + "kw_only=False, inherited=False, on_setattr=None, alias=None)", ) == e.value.args def test_kw_only(self): """ Converts all attributes, including base class' attributes, if `kw_only` is provided. Therefore, `kw_only` allows attributes with defaults to - preceed mandatory attributes. + precede mandatory attributes. Updates in the subclass *don't* affect the base class attributes. """ @attr.s - class B(object): + class B: b = attr.ib() for b_a in B.__attrs_attrs__: @@ -269,7 +278,7 @@ class TestTransformAttrs(object): If these is passed, use it and ignore body and base classes. """ - class Base(object): + class Base: z = attr.ib() class C(Base): @@ -288,7 +297,7 @@ class TestTransformAttrs(object): """ @attr.s(init=False, these={"x": attr.ib()}) - class C(object): + class C: x = 5 assert 5 == C().x @@ -302,21 +311,21 @@ class TestTransformAttrs(object): b = attr.ib(default=2) a = attr.ib(default=1) - @attr.s(these=ordered_dict([("a", a), ("b", b)])) - class C(object): + @attr.s(these={"a": a, "b": b}) + class C: pass assert "C(a=1, b=2)" == repr(C()) def test_multiple_inheritance_old(self): """ - Old multiple inheritance attributre collection behavior is retained. + Old multiple inheritance attribute collection behavior is retained. See #285 """ @attr.s - class A(object): + class A: a1 = attr.ib(default="a1") a2 = attr.ib(default="a2") @@ -351,7 +360,7 @@ class TestTransformAttrs(object): """ @attr.s(collect_by_mro=True) - class C(object): + class C: x = attr.ib(default=1) @attr.s(collect_by_mro=True) @@ -368,7 +377,7 @@ class TestTransformAttrs(object): """ @attr.s - class A(object): + class A: a1 = attr.ib(default="a1") a2 = attr.ib(default="a2") @@ -405,8 +414,7 @@ class TestTransformAttrs(object): """ @attr.s(collect_by_mro=True) - class A(object): - + class A: x = attr.ib(10) def xx(self): @@ -437,7 +445,7 @@ class TestTransformAttrs(object): """ @attr.s - class A(object): + class A: a = attr.ib() @attr.s @@ -461,31 +469,18 @@ class TestTransformAttrs(object): assert False is f(C).c.inherited -class TestAttributes(object): +class TestAttributes: """ Tests for the `attrs`/`attr.s` class decorator. """ - @pytest.mark.skipif(not PY2, reason="No old-style classes in Py3") - def test_catches_old_style(self): - """ - Raises TypeError on old-style classes. - """ - with pytest.raises(TypeError) as e: - - @attr.s - class C: - pass - - assert ("attrs only works with new-style classes.",) == e.value.args - def test_sets_attrs(self): """ Sets the `__attrs_attrs__` class attribute with a list of `Attribute`s. """ @attr.s - class C(object): + class C: x = attr.ib() assert "x" == C.__attrs_attrs__[0].name @@ -497,7 +492,7 @@ class TestAttributes(object): """ @attr.s - class C3(object): + class C3: pass assert "C3()" == repr(C3()) @@ -523,7 +518,7 @@ class TestAttributes(object): # overwritten afterwards. sentinel = object() - class C(object): + class C: x = attr.ib() setattr(C, method_name, sentinel) @@ -536,7 +531,7 @@ class TestAttributes(object): assert meth is None @pytest.mark.parametrize( - "arg_name, method_name", + ("arg_name", "method_name"), [ ("repr", "__repr__"), ("eq", "__eq__"), @@ -564,7 +559,7 @@ class TestAttributes(object): if arg_name == "eq": am_args["order"] = False - class C(object): + class C: x = attr.ib() setattr(C, method_name, sentinel) @@ -580,23 +575,22 @@ class TestAttributes(object): Otherwise, it does not. """ - class C(object): + class C: x = attr.ib() C = attr.s(init=init)(C) assert hasattr(C, "__attrs_init__") != init - @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.") @given(slots_outer=booleans(), slots_inner=booleans()) def test_repr_qualname(self, slots_outer, slots_inner): """ - On Python 3, the name in repr is the __qualname__. + The name in repr is the __qualname__. """ @attr.s(slots=slots_outer) - class C(object): + class C: @attr.s(slots=slots_inner) - class D(object): + class D: pass assert "C.D()" == repr(C.D()) @@ -609,38 +603,36 @@ class TestAttributes(object): """ @attr.s(slots=slots_outer) - class C(object): + class C: @attr.s(repr_ns="C", slots=slots_inner) - class D(object): + class D: pass assert "C.D()" == repr(C.D()) - @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.") @given(slots_outer=booleans(), slots_inner=booleans()) def test_name_not_overridden(self, slots_outer, slots_inner): """ - On Python 3, __name__ is different from __qualname__. + __name__ is different from __qualname__. """ @attr.s(slots=slots_outer) - class C(object): + class C: @attr.s(slots=slots_inner) - class D(object): + class D: pass assert C.D.__name__ == "D" assert C.D.__qualname__ == C.__qualname__ + ".D" - @pytest.mark.parametrize("with_validation", [True, False]) - def test_pre_init(self, with_validation, monkeypatch): + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_init(self): """ Verify that __attrs_pre_init__ gets called if defined. """ - monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s - class C(object): + class C: def __attrs_pre_init__(self2): self2.z = 30 @@ -648,15 +640,68 @@ class TestAttributes(object): assert 30 == getattr(c, "z", None) - @pytest.mark.parametrize("with_validation", [True, False]) - def test_post_init(self, with_validation, monkeypatch): + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_init_args(self): + """ + Verify that __attrs_pre_init__ gets called with extra args if defined. + """ + + @attr.s + class C: + x = attr.ib() + + def __attrs_pre_init__(self2, x): + self2.z = x + 1 + + c = C(x=10) + + assert 11 == getattr(c, "z", None) + + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_init_kwargs(self): + """ + Verify that __attrs_pre_init__ gets called with extra args and kwargs + if defined. + """ + + @attr.s + class C: + x = attr.ib() + y = attr.field(kw_only=True) + + def __attrs_pre_init__(self2, x, y): + self2.z = x + y + 1 + + c = C(10, y=11) + + assert 22 == getattr(c, "z", None) + + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_init_kwargs_only(self): + """ + Verify that __attrs_pre_init__ gets called with extra kwargs only if + defined. + """ + + @attr.s + class C: + y = attr.field(kw_only=True) + + def __attrs_pre_init__(self2, y): + self2.z = y + 1 + + c = C(y=11) + + assert 12 == getattr(c, "z", None) + + @pytest.mark.usefixtures("with_and_without_validation") + def test_post_init(self): """ Verify that __attrs_post_init__ gets called if defined. """ - monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s - class C(object): + class C: x = attr.ib() y = attr.ib() @@ -667,15 +712,14 @@ class TestAttributes(object): assert 30 == getattr(c, "z", None) - @pytest.mark.parametrize("with_validation", [True, False]) - def test_pre_post_init_order(self, with_validation, monkeypatch): + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_post_init_order(self): """ Verify that __attrs_post_init__ gets called if defined. """ - monkeypatch.setattr(_config, "_run_validators", with_validation) @attr.s - class C(object): + class C: x = attr.ib() def __attrs_pre_init__(self2): @@ -694,7 +738,7 @@ class TestAttributes(object): """ @attr.s - class C(object): + class C: x = attr.ib(type=int) y = attr.ib(type=str) z = attr.ib() @@ -703,14 +747,13 @@ class TestAttributes(object): assert str is fields(C).y.type assert None is fields(C).z.type - @pytest.mark.parametrize("slots", [True, False]) def test_clean_class(self, slots): """ Attribute definitions do not appear on the class body after @attr.s. """ @attr.s(slots=slots) - class C(object): + class C: x = attr.ib() x = getattr(C, "x", None) @@ -723,7 +766,7 @@ class TestAttributes(object): """ @attr.s - class C(object): + class C: x = attr.ib(factory=list) assert Factory(list) == attr.fields(C).x.default @@ -735,7 +778,7 @@ class TestAttributes(object): with pytest.raises(ValueError, match="mutually exclusive"): @attr.s - class C(object): + class C: x = attr.ib(factory=list, default=Factory(list)) def test_sugar_callable(self): @@ -746,7 +789,7 @@ class TestAttributes(object): with pytest.raises(ValueError, match="must be a callable"): @attr.s - class C(object): + class C: x = attr.ib(factory=Factory(list)) def test_inherited_does_not_affect_hashing_and_equality(self): @@ -756,7 +799,7 @@ class TestAttributes(object): """ @attr.s - class BaseClass(object): + class BaseClass: x = attr.ib() @attr.s @@ -770,7 +813,7 @@ class TestAttributes(object): assert hash(ba) == hash(sa) -class TestKeywordOnlyAttributes(object): +class TestKeywordOnlyAttributes: """ Tests for keyword-only attributes. """ @@ -781,7 +824,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class C(object): + class C: a = attr.ib() b = attr.ib(default=2, kw_only=True) c = attr.ib(kw_only=True) @@ -800,7 +843,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class C(object): + class C: x = attr.ib(init=False, default=0, kw_only=True) y = attr.ib() @@ -816,20 +859,15 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class C(object): + class C: x = attr.ib(kw_only=True) with pytest.raises(TypeError) as e: C() - if PY2: - assert ( - "missing required keyword-only argument: 'x'" - ) in e.value.args[0] - else: - assert ( - "missing 1 required keyword-only argument: 'x'" - ) in e.value.args[0] + assert ( + "missing 1 required keyword-only argument: 'x'" + ) in e.value.args[0] def test_keyword_only_attributes_unexpected(self): """ @@ -837,7 +875,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class C(object): + class C: x = attr.ib(kw_only=True) with pytest.raises(TypeError) as e: @@ -854,7 +892,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class C(object): + class C: a = attr.ib(kw_only=True) b = attr.ib(kw_only=True, default="b") c = attr.ib(kw_only=True) @@ -883,7 +921,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class Base(object): + class Base: x = attr.ib(default=0) @attr.s @@ -902,7 +940,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s(kw_only=True) - class C(object): + class C: x = attr.ib() y = attr.ib(kw_only=True) @@ -921,7 +959,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class Base(object): + class Base: x = attr.ib(default=0) @attr.s(kw_only=True) @@ -944,7 +982,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class KwArgBeforeInitFalse(object): + class KwArgBeforeInitFalse: kwarg = attr.ib(kw_only=True) non_init_function_default = attr.ib(init=False) non_init_keyword_default = attr.ib( @@ -972,7 +1010,7 @@ class TestKeywordOnlyAttributes(object): """ @attr.s - class KwArgBeforeInitFalseParent(object): + class KwArgBeforeInitFalseParent: kwarg = attr.ib(kw_only=True) @attr.s @@ -993,34 +1031,14 @@ class TestKeywordOnlyAttributes(object): assert c.non_init_keyword_default == "default-by-keyword" -@pytest.mark.skipif(not PY2, reason="PY2-specific keyword-only error behavior") -class TestKeywordOnlyAttributesOnPy2(object): - """ - Tests for keyword-only attribute behavior on py2. - """ - - def test_no_init(self): - """ - Keyworld-only is a no-op, not any error, if ``init=false``. - """ - - @attr.s(kw_only=True, init=False) - class ClassLevel(object): - a = attr.ib() - - @attr.s(init=False) - class AttrLevel(object): - a = attr.ib(kw_only=True) - - @attr.s -class GC(object): +class GC: @attr.s - class D(object): + class D: pass -class TestMakeClass(object): +class TestMakeClass: """ Tests for `make_class`. """ @@ -1033,7 +1051,7 @@ class TestMakeClass(object): C1 = make_class("C1", ls(["a", "b"])) @attr.s - class C2(object): + class C2: a = attr.ib() b = attr.ib() @@ -1048,7 +1066,7 @@ class TestMakeClass(object): ) @attr.s - class C2(object): + class C2: a = attr.ib(default=42) b = attr.ib(default=None) @@ -1076,7 +1094,7 @@ class TestMakeClass(object): Parameter bases default to (object,) and subclasses correctly """ - class D(object): + class D: pass cls = make_class("C", {}) @@ -1088,7 +1106,18 @@ class TestMakeClass(object): assert D in cls.__mro__ assert isinstance(cls(), D) - @pytest.mark.parametrize("slots", [True, False]) + def test_additional_class_body(self): + """ + Additional class_body is added to newly created class. + """ + + def echo_func(cls, *args): + return args + + cls = make_class("C", {}, class_body={"echo": classmethod(echo_func)}) + + assert ("a", "b") == cls.echo("a", "b") + def test_clean_class(self, slots): """ Attribute definitions do not appear on the class body. @@ -1116,11 +1145,10 @@ class TestMakeClass(object): b = attr.ib(default=2) a = attr.ib(default=1) - C = attr.make_class("C", ordered_dict([("a", a), ("b", b)])) + C = attr.make_class("C", {"a": a, "b": b}) assert "C(a=1, b=2)" == repr(C()) - @pytest.mark.skipif(PY2, reason="Python 3-only") def test_generic_dynamic_class(self): """ make_class can create generic dynamic classes. @@ -1137,7 +1165,7 @@ class TestMakeClass(object): attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],)) -class TestFields(object): +class TestFields: """ Tests for `fields`. """ @@ -1154,13 +1182,29 @@ class TestFields(object): def test_handler_non_attrs_class(self): """ - Raises `ValueError` if passed a non-``attrs`` instance. + Raises `ValueError` if passed a non-*attrs* instance. """ with pytest.raises(NotAnAttrsClassError) as e: fields(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." + ) == e.value.args[0] + + def test_handler_non_attrs_generic_class(self): + """ + Raises `ValueError` if passed a non-*attrs* generic class. + """ + T = TypeVar("T") + + class B(Generic[T]): + pass + + with pytest.raises(NotAnAttrsClassError) as e: + fields(B[str]) + + assert ( + f"{B[str]!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1178,8 +1222,26 @@ class TestFields(object): for attribute in fields(C): assert getattr(fields(C), attribute.name) is attribute + def test_generics(self): + """ + Fields work with generic classes. + """ + T = TypeVar("T") + + @attr.define + class A(Generic[T]): + a: T + + assert len(fields(A)) == 1 + assert fields(A).a.name == "a" + assert fields(A).a.default is attr.NOTHING + + assert len(fields(A[str])) == 1 + assert fields(A[str]).a.name == "a" + assert fields(A[str]).a.default is attr.NOTHING -class TestFieldsDict(object): + +class TestFieldsDict: """ Tests for `fields_dict`. """ @@ -1196,13 +1258,13 @@ class TestFieldsDict(object): def test_handler_non_attrs_class(self): """ - Raises `ValueError` if passed a non-``attrs`` instance. + Raises `ValueError` if passed a non-*attrs* instance. """ with pytest.raises(NotAnAttrsClassError) as e: fields_dict(object) assert ( - "{o!r} is not an attrs-decorated class.".format(o=object) + f"{object!r} is not an attrs-decorated class." ) == e.value.args[0] @given(simple_classes()) @@ -1212,12 +1274,12 @@ class TestFieldsDict(object): """ d = fields_dict(C) - assert isinstance(d, ordered_dict) + assert isinstance(d, dict) assert list(fields(C)) == list(d.values()) - assert [a.name for a in fields(C)] == [field_name for field_name in d] + assert [a.name for a in fields(C)] == list(d) -class TestConverter(object): +class TestConverter: """ Tests for attribute conversion. """ @@ -1260,19 +1322,14 @@ class TestConverter(object): """ C = make_class( "C", - ordered_dict( - [ - ("y", attr.ib()), - ( - "x", - attr.ib( - init=init, - default=Factory(lambda: val), - converter=lambda v: v + 1, - ), - ), - ] - ), + { + "y": attr.ib(), + "x": attr.ib( + init=init, + default=Factory(lambda: val), + converter=lambda v: v + 1, + ), + }, ) c = C(2) @@ -1330,7 +1387,7 @@ class TestConverter(object): C("1") -class TestValidate(object): +class TestValidate: """ Tests for `validate`. """ @@ -1424,11 +1481,11 @@ class TestValidate(object): # Hypothesis seems to cache values, so the lists of attributes come out # unsorted. sorted_lists_of_attrs = list_of_attrs.map( - lambda l: sorted(l, key=attrgetter("counter")) + lambda ln: sorted(ln, key=attrgetter("counter")) ) -class TestMetadata(object): +class TestMetadata: """ Tests for metadata handling. """ @@ -1471,8 +1528,8 @@ class TestMetadata(object): a.metadata.setdefault(string, string) for k in a.metadata: - # For some reason, Python 3's MappingProxyType throws an - # IndexError for deletes on a large integer key. + # For some reason, MappingProxyType throws an IndexError for + # deletes on a large integer key. with pytest.raises((TypeError, IndexError)): del a.metadata[k] with pytest.raises(AttributeError): @@ -1523,7 +1580,7 @@ class TestMetadata(object): assert md is a.metadata -class TestClassBuilder(object): +class TestClassBuilder: """ Tests for `_ClassBuilder`. """ @@ -1545,7 +1602,7 @@ class TestClassBuilder(object): repr of builder itself makes sense. """ - class C(object): + class C: pass b = _ClassBuilder( @@ -1572,7 +1629,7 @@ class TestClassBuilder(object): All methods return the builder for chaining. """ - class C(object): + class C: x = attr.ib() b = _ClassBuilder( @@ -1627,12 +1684,12 @@ class TestClassBuilder(object): """ @attr.s(hash=True, str=True) - class C(object): + class C: def organic(self): pass @attr.s(hash=True, str=True) - class D(object): + class D: pass meth_C = getattr(C, meth_name) @@ -1640,11 +1697,10 @@ class TestClassBuilder(object): assert meth_name == meth_C.__name__ == meth_D.__name__ assert C.organic.__module__ == meth_C.__module__ == meth_D.__module__ - if not PY2: - # This is assertion that would fail if a single __ne__ instance - # was reused across multiple _make_eq calls. - organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] - assert organic_prefix + "." + meth_name == meth_C.__qualname__ + # This is assertion that would fail if a single __ne__ instance + # was reused across multiple _make_eq calls. + organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0] + assert organic_prefix + "." + meth_name == meth_C.__qualname__ def test_handles_missing_meta_on_class(self): """ @@ -1652,7 +1708,7 @@ class TestClassBuilder(object): either. """ - class C(object): + class C: pass b = _ClassBuilder( @@ -1691,7 +1747,7 @@ class TestClassBuilder(object): """ @attr.s(slots=True) - class C(object): + class C: __weakref__ = attr.ib( init=False, hash=False, repr=False, eq=False, order=False ) @@ -1705,7 +1761,7 @@ class TestClassBuilder(object): """ @attr.s(slots=True) - class C(object): + class C: pass @attr.s(slots=True) @@ -1748,7 +1804,7 @@ class TestClassBuilder(object): """ @attr.s(eq=True, **kwargs) - class C(object): + class C: x = attr.ib() a = C(1) @@ -1763,7 +1819,7 @@ class TestClassBuilder(object): """ @attr.s(eq=True, **kwargs) - class C(object): + class C: x = attr.ib() def __getstate__(self): @@ -1779,6 +1835,107 @@ class TestClassBuilder(object): assert actual == expected +class TestInitAlias: + """ + Tests for Attribute alias handling. + """ + + def test_default_and_specify(self): + """ + alias is present on the Attributes returned from attr.fields. + + If left unspecified, it defaults to standard private-attribute + handling. If specified, it passes through the explicit alias. + """ + + # alias is None by default on _CountingAttr + default_counting = attr.ib() + assert default_counting.alias is None + + override_counting = attr.ib(alias="specified") + assert override_counting.alias == "specified" + + @attr.s + class Cases: + public_default = attr.ib() + _private_default = attr.ib() + __dunder_default__ = attr.ib() + + public_override = attr.ib(alias="public") + _private_override = attr.ib(alias="_private") + __dunder_override__ = attr.ib(alias="__dunder__") + + cases = attr.fields_dict(Cases) + + # Default applies private-name mangling logic + assert cases["public_default"].name == "public_default" + assert cases["public_default"].alias == "public_default" + + assert cases["_private_default"].name == "_private_default" + assert cases["_private_default"].alias == "private_default" + + assert cases["__dunder_default__"].name == "__dunder_default__" + assert cases["__dunder_default__"].alias == "dunder_default__" + + # Override is passed through + assert cases["public_override"].name == "public_override" + assert cases["public_override"].alias == "public" + + assert cases["_private_override"].name == "_private_override" + assert cases["_private_override"].alias == "_private" + + assert cases["__dunder_override__"].name == "__dunder_override__" + assert cases["__dunder_override__"].alias == "__dunder__" + + # And aliases are applied to the __init__ signature + example = Cases( + public_default=1, + private_default=2, + dunder_default__=3, + public=4, + _private=5, + __dunder__=6, + ) + + assert example.public_default == 1 + assert example._private_default == 2 + assert example.__dunder_default__ == 3 + assert example.public_override == 4 + assert example._private_override == 5 + assert example.__dunder_override__ == 6 + + def test_evolve(self): + """ + attr.evolve uses Attribute.alias to determine parameter names. + """ + + @attr.s + class EvolveCase: + _override = attr.ib(alias="_override") + __mangled = attr.ib() + __dunder__ = attr.ib() + + org = EvolveCase(1, 2, 3) + + # Previous behavior of evolve as broken for double-underscore + # passthrough, and would raise here due to mis-mapping the __dunder__ + # alias + assert attr.evolve(org) == org + + # evolve uses the alias to match __init__ signature + assert attr.evolve( + org, + _override=0, + ) == EvolveCase(0, 2, 3) + + # and properly passes through dunders and mangles + assert attr.evolve( + org, + EvolveCase__mangled=4, + dunder__=5, + ) == EvolveCase(1, 4, 5) + + class TestMakeOrder: """ Tests for _make_order(). @@ -1788,11 +1945,11 @@ class TestMakeOrder: """ Calling comparison methods on subclasses raises a TypeError. - We use the actual operation so we get an error raised on Python 3. + We use the actual operation so we get an error raised. """ @attr.s - class A(object): + class A: a = attr.ib() @attr.s @@ -1815,21 +1972,20 @@ class TestMakeOrder: == a.__ge__(b) ) - if not PY2: - with pytest.raises(TypeError): - a <= b + with pytest.raises(TypeError): + a <= b - with pytest.raises(TypeError): - a >= b + with pytest.raises(TypeError): + a >= b - with pytest.raises(TypeError): - a < b + with pytest.raises(TypeError): + a < b - with pytest.raises(TypeError): - a > b + with pytest.raises(TypeError): + a > b -class TestDetermineAttrsEqOrder(object): +class TestDetermineAttrsEqOrder: def test_default(self): """ If all are set to None, set both eq and order to the passed default. @@ -1865,7 +2021,7 @@ class TestDetermineAttrsEqOrder(object): _determine_attrs_eq_order(cmp, eq, order, True) -class TestDetermineAttribEqOrder(object): +class TestDetermineAttribEqOrder: def test_default(self): """ If all are set to None, set both eq and order to the passed default. @@ -1953,37 +2109,25 @@ class TestDocs: """ @attr.s - class A(object): + class A: pass if hasattr(A, "__qualname__"): method = getattr(A, meth_name) - expected = "Method generated by attrs for class {}.".format( - A.__qualname__ - ) + expected = f"Method generated by attrs for class {A.__qualname__}." assert expected == method.__doc__ -@pytest.mark.skipif(not PY2, reason="Needs to be only caught on Python 2.") -def test_auto_detect_raises_on_py2(): - """ - Trying to pass auto_detect=True to attr.s raises PythonTooOldError. - """ - with pytest.raises(PythonTooOldError): - attr.s(auto_detect=True) - - -class BareC(object): +class BareC: pass -class BareSlottedC(object): +class BareSlottedC: __slots__ = () -@pytest.mark.skipif(PY2, reason="Auto-detection is Python 3-only.") class TestAutoDetect: - @pytest.mark.parametrize("C", (BareC, BareSlottedC)) + @pytest.mark.parametrize("C", [BareC, BareSlottedC]) def test_determine_detects_non_presence_correctly(self, C): """ On an empty class, nothing should be detected. @@ -2001,8 +2145,6 @@ class TestAutoDetect: C, None, True, ("__le__", "__lt__", "__ge__", "__gt__") ) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_make_all_by_default(self, slots, frozen): """ If nothing is there to be detected, imply init=True, repr=True, @@ -2010,7 +2152,7 @@ class TestAutoDetect: """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() i = C(1) @@ -2025,15 +2167,13 @@ class TestAutoDetect: assert i.__ge__ is not o.__ge__ assert i.__gt__ is not o.__gt__ - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_detect_auto_init(self, slots, frozen): """ If auto_detect=True and an __init__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class CI(object): + class CI: x = attr.ib() def __init__(self): @@ -2041,15 +2181,13 @@ class TestAutoDetect: assert 42 == CI().x - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_detect_auto_repr(self, slots, frozen): """ If auto_detect=True and an __repr__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __repr__(self): @@ -2057,15 +2195,32 @@ class TestAutoDetect: assert "hi" == repr(C(42)) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) + def test_hash_uses_eq(self, slots, frozen): + """ + If eq is passed in, then __hash__ should use the eq callable + to generate the hash code. + """ + + @attr.s(slots=slots, frozen=frozen, hash=True) + class C: + x = attr.ib(eq=str) + + @attr.s(slots=slots, frozen=frozen, hash=True) + class D: + x = attr.ib() + + # These hashes should be the same because 1 is turned into + # string before hashing. + assert hash(C("1")) == hash(C(1)) + assert hash(D("1")) != hash(D(1)) + def test_detect_auto_hash(self, slots, frozen): """ If auto_detect=True and an __hash__ exists, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __hash__(self): @@ -2073,15 +2228,13 @@ class TestAutoDetect: assert 0xC0FFEE == hash(C(42)) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_detect_auto_eq(self, slots, frozen): """ If auto_detect=True and an __eq__ or an __ne__, exist, don't write one. """ @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __eq__(self, o): @@ -2091,7 +2244,7 @@ class TestAutoDetect: C(1) == C(1) @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class D(object): + class D: x = attr.ib() def __ne__(self, o): @@ -2100,8 +2253,6 @@ class TestAutoDetect: with pytest.raises(ValueError, match="worked"): D(1) != D(1) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_detect_auto_order(self, slots, frozen): """ If auto_detect=True and an __ge__, __gt__, __le__, or and __lt__ exist, @@ -2127,19 +2278,19 @@ class TestAutoDetect: assert_not_set(cls, ex, "__" + m + "__") @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class LE(object): + class LE: __le__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class LT(object): + class LT: __lt__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class GE(object): + class GE: __ge__ = 42 @attr.s(auto_detect=True, slots=slots, frozen=frozen) - class GT(object): + class GT: __gt__ = 42 assert_none_set(LE, "__le__") @@ -2147,15 +2298,13 @@ class TestAutoDetect: assert_none_set(GE, "__ge__") assert_none_set(GT, "__gt__") - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_override_init(self, slots, frozen): """ If init=True is passed, ignore __init__. """ @attr.s(init=True, auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __init__(self): @@ -2163,15 +2312,13 @@ class TestAutoDetect: assert C(1) == C(1) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_override_repr(self, slots, frozen): """ If repr=True is passed, ignore __repr__. """ @attr.s(repr=True, auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __repr__(self): @@ -2179,15 +2326,13 @@ class TestAutoDetect: assert "C(x=1)" == repr(C(1)) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_override_hash(self, slots, frozen): """ If hash=True is passed, ignore __hash__. """ @attr.s(hash=True, auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __hash__(self): @@ -2195,15 +2340,13 @@ class TestAutoDetect: assert hash(C(1)) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) def test_override_eq(self, slots, frozen): """ If eq=True is passed, ignore __eq__ and __ne__. """ @attr.s(eq=True, auto_detect=True, slots=slots, frozen=frozen) - class C(object): + class C: x = attr.ib() def __eq__(self, o): @@ -2214,10 +2357,8 @@ class TestAutoDetect: assert C(1) == C(1) - @pytest.mark.parametrize("slots", [True, False]) - @pytest.mark.parametrize("frozen", [True, False]) @pytest.mark.parametrize( - "eq,order,cmp", + ("eq", "order", "cmp"), [ (True, None, None), (True, True, None), @@ -2243,7 +2384,7 @@ class TestAutoDetect: slots=slots, frozen=frozen, ) - class C(object): + class C: x = attr.ib() __le__ = __lt__ = __gt__ = __ge__ = meth @@ -2252,7 +2393,6 @@ class TestAutoDetect: assert C(2) > C(1) assert C(2) >= C(1) - @pytest.mark.parametrize("slots", [True, False]) @pytest.mark.parametrize("first", [True, False]) def test_total_ordering(self, slots, first): """ @@ -2262,7 +2402,7 @@ class TestAutoDetect: Ensure the order doesn't matter. """ - class C(object): + class C: x = attr.ib() own_eq_called = attr.ib(default=False) own_le_called = attr.ib(default=False) @@ -2301,21 +2441,22 @@ class TestAutoDetect: assert c1.own_eq_called - @pytest.mark.parametrize("slots", [True, False]) def test_detects_setstate_getstate(self, slots): """ __getstate__ and __setstate__ are not overwritten if either is present. """ @attr.s(slots=slots, auto_detect=True) - class C(object): + class C: def __getstate__(self): return ("hi",) - assert None is getattr(C(), "__setstate__", None) + assert getattr(object, "__setstate__", None) is getattr( + C, "__setstate__", None + ) @attr.s(slots=slots, auto_detect=True) - class C(object): + class C: called = attr.ib(False) def __setstate__(self, state): @@ -2328,7 +2469,9 @@ class TestAutoDetect: i.__setstate__(()) assert True is i.called - assert None is getattr(C(), "__getstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C, "__getstate__", None + ) @pytest.mark.skipif(PY310, reason="Pre-3.10 only.") def test_match_args_pre_310(self): @@ -2337,14 +2480,14 @@ class TestAutoDetect: """ @attr.s - class C(object): + class C: a = attr.ib() assert None is getattr(C, "__match_args__", None) @pytest.mark.skipif(not PY310, reason="Structural pattern matching is 3.10+") -class TestMatchArgs(object): +class TestMatchArgs: """ Tests for match_args and __match_args__ generation. """ diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_mypy.yml b/tests/wpt/tests/tools/third_party/attrs/tests/test_mypy.yml index ca17b0a662a..0d0757233b4 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_mypy.yml +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_mypy.yml @@ -1,26 +1,26 @@ - case: attr_s_with_type_argument parametrized: - - val: 'a = attr.ib(type=int)' - - val: 'a: int = attr.ib()' + - val: "a = attr.ib(type=int)" + - val: "a: int = attr.ib()" main: | import attr @attr.s class C: {{ val }} - C() # E: Missing positional argument "a" in call to "C" + C() # E: Missing positional argument "a" in call to "C" [call-arg] C(1) C(a=1) - C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" + C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" [arg-type] - case: attr_s_with_type_annotations - main : | + main: | import attr @attr.s class C: a: int = attr.ib() - C() # E: Missing positional argument "a" in call to "C" + C() # E: Missing positional argument "a" in call to "C" [call-arg] C(1) C(a=1) - C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" + C(a="hi") # E: Argument "a" to "C" has incompatible type "str"; expected "int" [arg-type] - case: testAttrsSimple main: | @@ -39,9 +39,10 @@ A(1, [2]) A(1, [2], '3', 4) A(1, 2, 3, 4) - A(1, [2], '3', 4, 5) # E: Too many arguments for "A" + A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [call-arg] - case: testAttrsAnnotated + regex: true main: | import attr from typing import List, ClassVar @@ -53,13 +54,14 @@ _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" + reveal_type(A) # N: Revealed type is "def \(a: builtins\.int, b: builtins\.list\[builtins\.int\], c: builtins\.str =, d: builtins\.int =\) -> main\.A" A(1, [2]) A(1, [2], '3', 4) - A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" - A(1, [2], '3', 4, 5) # E: Too many arguments for "A" + A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "[Ll]ist\[int\]" \[arg-type\] # E: Argument 3 to "A" has incompatible type "int"; expected "str" \[arg-type\] + A(1, [2], '3', 4, 5) # E: Too many arguments for "A" \[call-arg\] - case: testAttrsPython2Annotations + regex: true main: | import attr from typing import List, ClassVar @@ -71,13 +73,14 @@ _d = attr.ib(validator=None, default=18) # type: int E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" + reveal_type(A) # N: Revealed type is "def \(a: builtins\.int, b: builtins\.list\[builtins\.int\], c: builtins\.str =, d: builtins\.int =\) -> main\.A" A(1, [2]) A(1, [2], '3', 4) - A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" - A(1, [2], '3', 4, 5) # E: Too many arguments for "A" + A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "[Ll]ist\[int\]" \[arg-type\] # E: Argument 3 to "A" has incompatible type "int"; expected "str" \[arg-type\] + A(1, [2], '3', 4, 5) # E: Too many arguments for "A" \[call-arg\] - case: testAttrsAutoAttribs + regex: true main: | import attr from typing import List, ClassVar @@ -89,23 +92,23 @@ _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 - reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> main.A" + reveal_type(A) # N: Revealed type is "def \(a: builtins\.int, b: builtins.list\[builtins.int\], c: builtins\.str =, d: builtins\.int =\) -> main\.A" A(1, [2]) A(1, [2], '3', 4) - A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" - A(1, [2], '3', 4, 5) # E: Too many arguments for "A" + A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "[Ll]ist\[int\]" \[arg-type\] # E: Argument 3 to "A" has incompatible type "int"; expected "str" \[arg-type\] + A(1, [2], '3', 4, 5) # E: Too many arguments for "A" \[call-arg\] - case: testAttrsUntypedNoUntypedDefs mypy_config: | - disallow_untyped_defs = True + disallow_untyped_defs = True main: | import attr @attr.s class A: - a = attr.ib() # E: Need type annotation for "a" - _b = attr.ib() # E: Need type annotation for "_b" - c = attr.ib(18) # E: Need type annotation for "c" - _d = attr.ib(validator=None, default=18) # E: Need type annotation for "_d" + a = attr.ib() # E: Need type annotation for "a" [var-annotated] + _b = attr.ib() # E: Need type annotation for "_b" [var-annotated] + c = attr.ib(18) # E: Need type annotation for "c" [var-annotated] + _d = attr.ib(validator=None, default=18) # E: Need type annotation for "_d" [var-annotated] E = 18 - case: testAttrsWrongReturnValue @@ -115,24 +118,25 @@ class A: x: int = attr.ib(8) def foo(self) -> str: - return self.x # E: Incompatible return value type (got "int", expected "str") + return self.x # E: Incompatible return value type (got "int", expected "str") [return-value] @attr.s class B: x = attr.ib(8) # type: int def foo(self) -> str: - return self.x # E: Incompatible return value type (got "int", expected "str") + return self.x # E: Incompatible return value type (got "int", expected "str") [return-value] @attr.dataclass class C: x: int = 8 def foo(self) -> str: - return self.x # E: Incompatible return value type (got "int", expected "str") + return self.x # E: Incompatible return value type (got "int", expected "str") [return-value] @attr.s class D: x = attr.ib(8, type=int) def foo(self) -> str: - return self.x # E: Incompatible return value type (got "int", expected "str") + return self.x # E: Incompatible return value type (got "int", expected "str") [return-value] - case: testAttrsSeriousNames + regex: true main: | from attr import attrib, attrs from typing import List @@ -143,11 +147,11 @@ c = attrib(18) _d = attrib(validator=None, default=18) CLASS_VAR = 18 - reveal_type(A) # N: Revealed type is "def (a: Any, b: builtins.list[builtins.int], c: Any =, d: Any =) -> main.A" + reveal_type(A) # N: Revealed type is "def \(a: Any, b: builtins.list\[builtins.int\], c: Any =, d: Any =\) -> main\.A" A(1, [2]) A(1, [2], '3', 4) - A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" - A(1, [2], '3', 4, 5) # E: Too many arguments for "A" + A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "[Ll]ist\[int\]" \[arg-type\] + A(1, [2], '3', 4, 5) # E: Too many arguments for "A" \[call-arg\] - case: testAttrsDefaultErrors main: | @@ -155,19 +159,19 @@ @attr.s class A: x = attr.ib(default=17) - y = attr.ib() # E: Non-default attributes not allowed after default attributes. + y = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc] @attr.s(auto_attribs=True) class B: x: int = 17 - y: int # E: Non-default attributes not allowed after default attributes. + y: int # E: Non-default attributes not allowed after default attributes. [misc] @attr.s(auto_attribs=True) class C: x: int = attr.ib(default=17) - y: int # E: Non-default attributes not allowed after default attributes. + y: int # E: Non-default attributes not allowed after default attributes. [misc] @attr.s class D: x = attr.ib() - y = attr.ib() # E: Non-default attributes not allowed after default attributes. + y = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc] @x.default def foo(self): @@ -177,9 +181,9 @@ main: | import attr x = True - @attr.s(cmp=x) # E: "cmp" argument must be True or False. + @attr.s(cmp=x) # E: "cmp" argument must be a True, False, or None literal [literal-required] class A: - a = attr.ib(init=x) # E: "init" argument must be True or False. + a = attr.ib(init=x) # E: "init" argument must be a True or False literal [literal-required] - case: testAttrsInitFalse main: | @@ -192,8 +196,8 @@ _d: int = attrib(validator=None, default=18) reveal_type(A) # N: Revealed type is "def () -> main.A" A() - A(1, [2]) # E: Too many arguments for "A" - A(1, [2], '3', 4) # E: Too many arguments for "A" + A(1, [2]) # E: Too many arguments for "A" [call-arg] + A(1, [2], '3', 4) # E: Too many arguments for "A" [call-arg] - case: testAttrsInitAttribFalse main: | @@ -205,16 +209,17 @@ reveal_type(A) # N: Revealed type is "def (b: Any) -> main.A" - case: testAttrsCmpTrue + regex: true main: | from attr import attrib, attrs @attrs(auto_attribs=True) class A: a: int - reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A" - reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(A.__le__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(A.__gt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(A.__ge__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(A) # N: Revealed type is "def \(a: builtins.int\) -> main.A" + reveal_type(A.__lt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(A.__le__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(A.__gt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(A.__ge__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" A(1) < A(2) A(1) <= A(2) @@ -223,17 +228,17 @@ A(1) == A(2) A(1) != A(2) - A(1) < 1 # E: Unsupported operand types for < ("A" and "int") - A(1) <= 1 # E: Unsupported operand types for <= ("A" and "int") - A(1) > 1 # E: Unsupported operand types for > ("A" and "int") - A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int") + A(1) < 1 # E: Unsupported operand types for < \("A" and "int"\) \[operator\] + A(1) <= 1 # E: Unsupported operand types for <= \("A" and "int"\) \[operator\] + A(1) > 1 # E: Unsupported operand types for > \("A" and "int"\) \[operator\] + A(1) >= 1 # E: Unsupported operand types for >= \("A" and "int"\) \[operator\] A(1) == 1 A(1) != 1 - 1 < A(1) # E: Unsupported operand types for < ("int" and "A") - 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") - 1 > A(1) # E: Unsupported operand types for > ("int" and "A") - 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") + 1 < A(1) # E: Unsupported operand types for < \("int" and "A"\) \[operator\] + 1 <= A(1) # E: Unsupported operand types for <= \("int" and "A"\) \[operator\] + 1 > A(1) # E: Unsupported operand types for > \("int" and "A"\) \[operator\] + 1 >= A(1) # E: Unsupported operand types for >= \("int" and "A"\) \[operator\] 1 == A(1) 1 != A(1) @@ -247,24 +252,24 @@ reveal_type(A.__eq__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool" reveal_type(A.__ne__) # N: Revealed type is "def (builtins.object, builtins.object) -> builtins.bool" - A(1) < A(2) # E: Unsupported left operand type for < ("A") - A(1) <= A(2) # E: Unsupported left operand type for <= ("A") - A(1) > A(2) # E: Unsupported left operand type for > ("A") - A(1) >= A(2) # E: Unsupported left operand type for >= ("A") + A(1) < A(2) # E: Unsupported left operand type for < ("A") [operator] + A(1) <= A(2) # E: Unsupported left operand type for <= ("A") [operator] + A(1) > A(2) # E: Unsupported left operand type for > ("A") [operator] + A(1) >= A(2) # E: Unsupported left operand type for >= ("A") [operator] A(1) == A(2) A(1) != A(2) - A(1) < 1 # E: Unsupported operand types for > ("int" and "A") - A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") - A(1) > 1 # E: Unsupported operand types for < ("int" and "A") - A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") + A(1) < 1 # E: Unsupported operand types for > ("int" and "A") [operator] + A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") [operator] + A(1) > 1 # E: Unsupported operand types for < ("int" and "A") [operator] + A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") [operator] A(1) == 1 A(1) != 1 - 1 < A(1) # E: Unsupported operand types for < ("int" and "A") - 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") - 1 > A(1) # E: Unsupported operand types for > ("int" and "A") - 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") + 1 < A(1) # E: Unsupported operand types for < ("int" and "A") [operator] + 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") [operator] + 1 > A(1) # E: Unsupported operand types for > ("int" and "A") [operator] + 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") [operator] 1 == A(1) 1 != A(1) @@ -276,24 +281,24 @@ a: int reveal_type(A) # N: Revealed type is "def (a: builtins.int) -> main.A" - A(1) < A(2) # E: Unsupported left operand type for < ("A") - A(1) <= A(2) # E: Unsupported left operand type for <= ("A") - A(1) > A(2) # E: Unsupported left operand type for > ("A") - A(1) >= A(2) # E: Unsupported left operand type for >= ("A") + A(1) < A(2) # E: Unsupported left operand type for < ("A") [operator] + A(1) <= A(2) # E: Unsupported left operand type for <= ("A") [operator] + A(1) > A(2) # E: Unsupported left operand type for > ("A") [operator] + A(1) >= A(2) # E: Unsupported left operand type for >= ("A") [operator] A(1) == A(2) A(1) != A(2) - A(1) < 1 # E: Unsupported operand types for > ("int" and "A") - A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") - A(1) > 1 # E: Unsupported operand types for < ("int" and "A") - A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") + A(1) < 1 # E: Unsupported operand types for > ("int" and "A") [operator] + A(1) <= 1 # E: Unsupported operand types for >= ("int" and "A") [operator] + A(1) > 1 # E: Unsupported operand types for < ("int" and "A") [operator] + A(1) >= 1 # E: Unsupported operand types for <= ("int" and "A") [operator] A(1) == 1 A(1) != 1 - 1 < A(1) # E: Unsupported operand types for < ("int" and "A") - 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") - 1 > A(1) # E: Unsupported operand types for > ("int" and "A") - 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") + 1 < A(1) # E: Unsupported operand types for < ("int" and "A") [operator] + 1 <= A(1) # E: Unsupported operand types for <= ("int" and "A") [operator] + 1 > A(1) # E: Unsupported operand types for > ("int" and "A") [operator] + 1 >= A(1) # E: Unsupported operand types for >= ("int" and "A") [operator] 1 == A(1) 1 != A(1) @@ -308,15 +313,14 @@ class DeprecatedFalse: ... - @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order" + @attrs(cmp=False, eq=True) # E: Don't mix "cmp" with "eq" and "order" [misc] class Mixed: ... - @attrs(order=True, eq=False) # E: eq must be True if order is True + @attrs(order=True, eq=False) # E: eq must be True if order is True [misc] class Confused: ... - - case: testAttrsInheritance main: | import attr @@ -385,7 +389,7 @@ a = attr.ib() a = A(5) - a.a = 16 # E: Property "a" defined in "A" is read-only + a.a = 16 # E: Property "a" defined in "A" is read-only [misc] - case: testAttrsNextGenFrozen main: | from attr import frozen, field @@ -395,7 +399,7 @@ a = field() a = A(5) - a.a = 16 # E: Property "a" defined in "A" is read-only + a.a = 16 # E: Property "a" defined in "A" is read-only [misc] - case: testAttrsNextGenDetect main: | @@ -419,7 +423,6 @@ a: int b = field() - # TODO: Next Gen hasn't shipped with mypy yet so the following are wrong reveal_type(A) # N: Revealed type is "def (a: Any) -> main.A" reveal_type(B) # N: Revealed type is "def (a: builtins.int) -> main.B" reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: Any) -> main.C" @@ -453,6 +456,7 @@ reveal_type(A) # N: Revealed type is "def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> main.A" - case: testAttrsGeneric + regex: true main: | from typing import TypeVar, Generic, List import attr @@ -466,16 +470,15 @@ def bar(self) -> T: return self.x[0] def problem(self) -> T: - return self.x # E: Incompatible return value type (got "List[T]", expected "T") - reveal_type(A) # N: Revealed type is "def [T] (x: builtins.list[T`1], y: T`1) -> main.A[T`1]" + return self.x # E: Incompatible return value type \(got "[Ll]ist\[T\]", expected "T"\) \[return-value\] + reveal_type(A) # N: Revealed type is "def \[T\] \(x: builtins\.list\[T`1\], y: T`1\) -> main.A\[T`1\]" a = A([1], 2) - reveal_type(a) # N: Revealed type is "main.A[builtins.int*]" - reveal_type(a.x) # N: Revealed type is "builtins.list[builtins.int*]" - reveal_type(a.y) # N: Revealed type is "builtins.int*" - - A(['str'], 7) # E: Cannot infer type argument 1 of "A" - A([1], '2') # E: Cannot infer type argument 1 of "A" + reveal_type(a) # N: Revealed type is "main\.A\[builtins.int\]" + reveal_type(a.x) # N: Revealed type is "builtins\.list\[builtins\.int\]" + reveal_type(a.y) # N: Revealed type is "builtins\.int" + A(['str'], 7) # E: Cannot infer type argument 1 of "A" \[misc\] + A([1], '2') # E: Cannot infer type argument 1 of "A" \[misc\] - case: testAttrsUntypedGenericInheritance main: | @@ -514,12 +517,12 @@ pass sub_int = Sub[int](attr=1) - reveal_type(sub_int) # N: Revealed type is "main.Sub[builtins.int*]" - reveal_type(sub_int.attr) # N: Revealed type is "builtins.int*" + reveal_type(sub_int) # N: Revealed type is "main.Sub[builtins.int]" + reveal_type(sub_int.attr) # N: Revealed type is "builtins.int" sub_str = Sub[str](attr='ok') - reveal_type(sub_str) # N: Revealed type is "main.Sub[builtins.str*]" - reveal_type(sub_str.attr) # N: Revealed type is "builtins.str*" + reveal_type(sub_str) # N: Revealed type is "main.Sub[builtins.str]" + reveal_type(sub_str.attr) # N: Revealed type is "builtins.str" - case: testAttrsGenericInheritance2 main: | @@ -646,7 +649,7 @@ return cls(6, 'hello') @classmethod def bad(cls) -> A: - return cls(17) # E: Missing positional argument "b" in call to "A" + return cls(17) # E: Missing positional argument "b" in call to "A" [call-arg] def foo(self) -> int: return self.a reveal_type(A) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> main.A" @@ -688,7 +691,7 @@ main: | import attr @attr.s - class C(object): + class C: x: int = attr.ib(default=1) y: int = attr.ib() @y.default @@ -700,7 +703,7 @@ main: | import attr @attr.s - class C(object): + class C: x = attr.ib() @x.validator def check(self, attribute, value): @@ -764,8 +767,7 @@ return 'hello' - case: testAttrsUsingBadConverter - mypy_config: - strict_optional = False + regex: true main: | import attr from typing import overload @@ -785,15 +787,14 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:15: error: Cannot determine __init__ type from converter - main:15: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" - main:16: error: Cannot determine __init__ type from converter - main:16: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" - main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" + main:15: error: Cannot determine __init__ type from converter \[misc\] + main:15: error: Argument "converter" has incompatible type \"Callable\[\[\], str\]\"; expected (\"Callable\[\[Any\], Any\] \| None\"|\"Optional\[Callable\[\[Any\], Any\]\]\") \[arg-type\] + main:16: error: Cannot determine __init__ type from converter \[misc\] + main:16: error: Argument "converter" has incompatible type overloaded function; expected (\"Callable\[\[Any\], Any\] \| None\"|\"Optional\[Callable\[\[Any\], Any\]\]\") \[arg-type\] + main:17: note: Revealed type is "def (bad: Any, bad_overloaded: Any\) -> main.A" - case: testAttrsUsingBadConverterReprocess - mypy_config: - strict_optional = False + regex: true main: | import attr from typing import overload @@ -814,26 +815,26 @@ bad_overloaded: int = attr.ib(converter=bad_overloaded_converter) reveal_type(A) out: | - main:16: error: Cannot determine __init__ type from converter - main:16: error: Argument "converter" has incompatible type "Callable[[], str]"; expected "Callable[[Any], Any]" - main:17: error: Cannot determine __init__ type from converter - main:17: error: Argument "converter" has incompatible type overloaded function; expected "Callable[[Any], Any]" - main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any) -> main.A" + main:16: error: Cannot determine __init__ type from converter \[misc\] + main:16: error: Argument \"converter\" has incompatible type \"Callable\[\[\], str\]\"; expected (\"Callable\[\[Any\], Any\] \| None\"|\"Optional\[Callable\[\[Any\], Any\]\]\") \[arg-type\] + main:17: error: Cannot determine __init__ type from converter \[misc\] + main:17: error: Argument "converter" has incompatible type overloaded function; expected (\"Callable\[\[Any\], Any\] \| None\"|\"Optional\[Callable\[\[Any\], Any\]\]\") \[arg-type\] + main:18: note: Revealed type is "def (bad: Any, bad_overloaded: Any\) -> main.A" - case: testAttrsUsingUnsupportedConverter main: | import attr class Thing: def do_it(self, int) -> str: - ... + return "" thing = Thing() def factory(default: int): - ... + return 1 @attr.s class C: - x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions and types are currently supported - y: str = attr.ib(converter=lambda x: x) # E: Unsupported converter, only named functions and types are currently supported - z: str = attr.ib(converter=factory(8)) # E: Unsupported converter, only named functions and types are currently supported + x: str = attr.ib(converter=thing.do_it) # E: Unsupported converter, only named functions, types and lambdas are currently supported [misc] + y: str = attr.ib(converter=lambda x: x) + z: str = attr.ib(converter=factory(8)) # E: Unsupported converter, only named functions, types and lambdas are currently supported [misc] reveal_type(C) # N: Revealed type is "def (x: Any, y: Any, z: Any) -> main.C" - case: testAttrsUsingConverterAndSubclass @@ -874,6 +875,7 @@ o = C(1, 2, "3") - case: testAttrsCmpWithSubclasses + regex: true main: | import attr @attr.s @@ -885,29 +887,29 @@ @attr.s class D(A): pass - reveal_type(A.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(B.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(C.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" - reveal_type(D.__lt__) # N: Revealed type is "def [_AT] (self: _AT`-1, other: _AT`-1) -> builtins.bool" + reveal_type(A.__lt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(B.__lt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(C.__lt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" + reveal_type(D.__lt__) # N: Revealed type is "def \[_AT\] \(self: _AT`\d+, other: _AT`\d+\) -> builtins.bool" A() < A() B() < B() - A() < B() # E: Unsupported operand types for < ("A" and "B") + A() < B() # E: Unsupported operand types for < \("A" and "B"\) \[operator\] C() > A() C() > B() C() > C() - C() > D() # E: Unsupported operand types for > ("C" and "D") + C() > D() # E: Unsupported operand types for > \("C" and "D"\) \[operator\] D() >= A() - D() >= B() # E: Unsupported operand types for >= ("D" and "B") - D() >= C() # E: Unsupported operand types for >= ("D" and "C") + D() >= B() # E: Unsupported operand types for >= \("D" and "B"\) \[operator\] + D() >= C() # E: Unsupported operand types for >= \("D" and "C"\) \[operator\] D() >= D() - A() <= 1 # E: Unsupported operand types for <= ("A" and "int") - B() <= 1 # E: Unsupported operand types for <= ("B" and "int") - C() <= 1 # E: Unsupported operand types for <= ("C" and "int") - D() <= 1 # E: Unsupported operand types for <= ("D" and "int") + A() <= 1 # E: Unsupported operand types for <= \("A" and "int"\) \[operator\] + B() <= 1 # E: Unsupported operand types for <= \("B" and "int"\) \[operator\] + C() <= 1 # E: Unsupported operand types for <= \("C" and "int"\) \[operator\] + D() <= 1 # E: Unsupported operand types for <= \("D" and "int"\) \[operator\] - case: testAttrsComplexSuperclass main: | @@ -938,16 +940,16 @@ import attr @attr.s class A: - x = y = z = attr.ib() # E: Too many names for one attribute + x = y = z = attr.ib() # E: Too many names for one attribute [misc] - case: testAttrsPrivateInit main: | import attr @attr.s - class C(object): + class C: _x = attr.ib(init=False, default=42) C() - C(_x=42) # E: Unexpected keyword argument "_x" for "C" + C(_x=42) # E: Unexpected keyword argument "_x" for "C" [call-arg] - case: testAttrsAutoMustBeAll main: | @@ -957,9 +959,9 @@ a: int b = 17 # The following forms are not allowed with auto_attribs=True - c = attr.ib() # E: Need type annotation for "c" - d, e = attr.ib(), attr.ib() # E: Need type annotation for "d" # E: Need type annotation for "e" - f = g = attr.ib() # E: Need type annotation for "f" # E: Need type annotation for "g" + c = attr.ib() # E: Need type annotation for "c" [var-annotated] + d, e = attr.ib(), attr.ib() # E: Need type annotation for "d" [var-annotated] # E: Need type annotation for "e" [var-annotated] + f = g = attr.ib() # E: Need type annotation for "f" [var-annotated] # E: Need type annotation for "g" [var-annotated] - case: testAttrsRepeatedName main: | @@ -974,38 +976,15 @@ class B: a: int = attr.ib(default=8) b: int = attr.ib() - a: int = attr.ib() # E: Name "a" already defined on line 10 + a: int = attr.ib() # E: Name "a" already defined on line 10 [no-redef] reveal_type(B) # N: Revealed type is "def (b: builtins.int, a: builtins.int) -> main.B" @attr.s(auto_attribs=True) class C: a: int = 8 b: int - a: int = attr.ib() # E: Name "a" already defined on line 16 + a: int = attr.ib() # E: Name "a" already defined on line 16 [no-redef] reveal_type(C) # N: Revealed type is "def (a: builtins.int, b: builtins.int) -> main.C" -- case: testAttrsNewStyleClassPy2 - mypy_config: - python_version = 2.7 - main: | - import attr - @attr.s - class Good(object): - pass - @attr.s - class Bad: # E: attrs only works with new-style classes - pass - skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47 - -- case: testAttrsAutoAttribsPy2 - mypy_config: | - python_version = 2.7 - main: | - import attr - @attr.s(auto_attribs=True) # E: auto_attribs is not supported in Python 2 - class A(object): - x = attr.ib() - skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47 - - case: testAttrsFrozenSubclass main: | import attr @@ -1034,19 +1013,19 @@ non_frozen_base = NonFrozenBase(1) non_frozen_base.a = 17 frozen_base = FrozenBase(1) - frozen_base.a = 17 # E: Property "a" defined in "FrozenBase" is read-only + frozen_base.a = 17 # E: Property "a" defined in "FrozenBase" is read-only [misc] a = FrozenNonFrozen(1, 2) - a.a = 17 # E: Property "a" defined in "FrozenNonFrozen" is read-only - a.b = 17 # E: Property "b" defined in "FrozenNonFrozen" is read-only + a.a = 17 # E: Property "a" defined in "FrozenNonFrozen" is read-only [misc] + a.b = 17 # E: Property "b" defined in "FrozenNonFrozen" is read-only [misc] b = FrozenFrozen(1, 2) - b.a = 17 # E: Property "a" defined in "FrozenFrozen" is read-only - b.b = 17 # E: Property "b" defined in "FrozenFrozen" is read-only + b.a = 17 # E: Property "a" defined in "FrozenFrozen" is read-only [misc] + b.b = 17 # E: Property "b" defined in "FrozenFrozen" is read-only [misc] c = NonFrozenFrozen(1, 2) - c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only - c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only + c.a = 17 # E: Property "a" defined in "NonFrozenFrozen" is read-only [misc] + c.b = 17 # E: Property "b" defined in "NonFrozenFrozen" is read-only [misc] - case: testAttrsCallableAttributes main: | from typing import Callable @@ -1088,17 +1067,18 @@ import attr @attr.s class A: - x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory". + x: int = attr.ib(factory=int, default=7) # E: Can't pass both "default" and "factory". [misc] - case: testAttrsFactoryBadReturn + regex: true main: | import attr def my_factory() -> int: return 7 @attr.s class A: - x: int = attr.ib(factory=list) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") - y: str = attr.ib(factory=my_factory) # E: Incompatible types in assignment (expression has type "int", variable has type "str") + x: int = attr.ib(factory=list) # E: Incompatible types in assignment \(expression has type "[Ll]ist\[.*\]", variable has type "int"\) \[assignment\] + y: str = attr.ib(factory=my_factory) # E: Incompatible types in assignment \(expression has type "int", variable has type "str"\) \[assignment\] - case: testAttrsDefaultAndInit main: | @@ -1110,7 +1090,7 @@ b = attr.ib() # Ok because previous attribute is init=False c = attr.ib(default=44) d = attr.ib(init=False) # Ok because this attribute is init=False - e = attr.ib() # E: Non-default attributes not allowed after default attributes. + e = attr.ib() # E: Non-default attributes not allowed after default attributes. [misc] - case: testAttrsOptionalConverter main: | @@ -1149,8 +1129,8 @@ @attr.s class A: a = attr.ib(kw_only=True) - A() # E: Missing named argument "a" for "A" - A(15) # E: Too many positional arguments for "A" + A() # E: Missing named argument "a" for "A" [call-arg] + A(15) # E: Too many positional arguments for "A" [misc] A(a=15) - case: testAttrsKwOnlyClass @@ -1160,7 +1140,7 @@ class A: a: int b: bool - A() # E: Missing named argument "a" for "A" # E: Missing named argument "b" for "A" + A() # E: Missing named argument "a" for "A" [call-arg] # E: Missing named argument "b" for "A" [call-arg] A(b=True, a=15) - case: testAttrsKwOnlyClassNoInit @@ -1192,7 +1172,6 @@ c = attr.ib(15) D(b=17) - - case: testAttrsKwOnlySubclass main: | import attr @@ -1218,19 +1197,6 @@ a = attr.ib(kw_only=True) b = attr.ib(15) -- case: testAttrsKwOnlyPy2 - mypy_config: - python_version=2.7 - main: | - import attr - @attr.s(kw_only=True) # E: kw_only is not supported in Python 2 - class A(object): - x = attr.ib() - @attr.s - class B(object): - x = attr.ib(kw_only=True) # E: kw_only is not supported in Python 2 - skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47 - - case: testAttrsDisallowUntypedWorksForward main: | # flags: --disallow-untyped-defs @@ -1247,14 +1213,13 @@ reveal_type(B) # N: Revealed type is "def (x: main.C) -> main.B" - case: testDisallowUntypedWorksForwardBad - mypy_config: - disallow_untyped_defs = True + mypy_config: disallow_untyped_defs = True main: | import attr @attr.s class B: - x = attr.ib() # E: Need type annotation for "x" + x = attr.ib() # E: Need type annotation for "x" [var-annotated] reveal_type(B) # N: Revealed type is "def (x: Any) -> main.B" @@ -1264,7 +1229,7 @@ import attr @attr.s - class C(object): + class C: x: int = attr.ib(default=1) y: int = attr.ib() @y.default @@ -1279,7 +1244,7 @@ import attr @attr.s - class C(object): + class C: x = attr.ib() @x.validator def check(self, attribute, value): @@ -1296,7 +1261,7 @@ @attr.s class C: - total = attr.ib(type=Bad) # E: Name "Bad" is not defined + total = attr.ib(type=Bad) # E: Name "Bad" is not defined [name-defined] - case: testTypeInAttrForwardInRuntime main: | @@ -1307,7 +1272,7 @@ total = attr.ib(type=Forward) reveal_type(C.total) # N: Revealed type is "main.Forward" - C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "Forward" + C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "Forward" [arg-type] class Forward: ... - case: testDefaultInAttrForward @@ -1318,11 +1283,11 @@ class C: total = attr.ib(default=func()) - def func() -> int: ... + def func() -> int: return 5 C() C(1) - C(1, 2) # E: Too many arguments for "C" + C(1, 2) # E: Too many arguments for "C" [call-arg] - case: testTypeInAttrUndefinedFrozen main: | @@ -1330,9 +1295,9 @@ @attr.s(frozen=True) class C: - total = attr.ib(type=Bad) # E: Name "Bad" is not defined + total = attr.ib(type=Bad) # E: Name "Bad" is not defined [name-defined] - C(0).total = 1 # E: Property "total" defined in "C" is read-only + C(0).total = 1 # E: Property "total" defined in "C" is read-only [misc] - case: testTypeInAttrDeferredStar main: | @@ -1349,8 +1314,8 @@ class C: total = attr.ib(type=int) - C() # E: Missing positional argument "total" in call to "C" - C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "int" + C() # E: Missing positional argument "total" in call to "C" [call-arg] + C('no') # E: Argument 1 to "C" has incompatible type "str"; expected "int" [arg-type] - path: other.py content: | import lib @@ -1365,7 +1330,7 @@ from b import A1, A2 @attr.s - class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes. + class Asdf(A1, A2): # E: Non-default attributes not allowed after default attributes. [misc] pass - path: b.py content: | @@ -1393,3 +1358,57 @@ foo = x reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> main.B" + +- case: testFields + regex: true + main: | + from attrs import define, fields + + @define + class A: + a: int + b: str + + reveal_type(fields(A)) # N: Revealed type is "[Tt]uple\[attr.Attribute\[builtins.int\], attr.Attribute\[builtins.str\], fallback=main.A.__main_A_AttrsAttributes__\]" + +- case: testFieldsError + regex: true + main: | + from attrs import fields + + class A: + a: int + b: str + + fields(A) # E: Argument 1 to "fields" has incompatible type "[Tt]ype\[A\]"; expected "[Tt]ype\[AttrsInstance\]" \[arg-type\] + +- case: testAsDict + main: | + from attrs import asdict, define + + @define + class A: + a: int + + asdict(A(1)) + +- case: testAsDictError + main: | + from attrs import asdict + + class A: + a: int + + asdict(A()) # E: Argument 1 to "asdict" has incompatible type "A"; expected "AttrsInstance" [arg-type] + +- case: testHasTypeGuard + main: | + from attrs import define, has + + @define + class A: + pass + + reveal_type(A) # N: Revealed type is "def () -> main.A" + if has(A): + reveal_type(A) # N: Revealed type is "Type[attr.AttrsInstance]" diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_next_gen.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_next_gen.py index 8395f9c0286..7d053d21439 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_next_gen.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_next_gen.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: MIT """ -Python 3-only integration tests for provisional next generation APIs. +Integration tests for next-generation APIs. """ import re +from contextlib import contextmanager from functools import partial import pytest @@ -27,6 +28,16 @@ class TestNextGen: """ C("1", 2) + def test_field_type(self): + """ + Make class with attrs.field and type parameter. + """ + classFields = {"testint": attrs.field(type=int)} + + A = attrs.make_class("A", classFields) + + assert int == attrs.fields(A).testint.type + def test_no_slots(self): """ slots can be deactivated. @@ -38,7 +49,7 @@ class TestNextGen: ns = NoSlots(1) - assert {"x": 1} == getattr(ns, "__dict__") + assert {"x": 1} == ns.__dict__ def test_validates(self): """ @@ -312,6 +323,38 @@ class TestNextGen: assert "foo" == ei.value.x assert ei.value.__cause__ is None + @pytest.mark.parametrize( + "decorator", + [ + partial(_attr.s, frozen=True, slots=True, auto_exc=True), + attrs.frozen, + attrs.define, + attrs.mutable, + ], + ) + def test_setting_traceback_on_exception(self, decorator): + """ + contextlib.contextlib (re-)sets __traceback__ on raised exceptions. + + Ensure that works, as well as if done explicitly + """ + + @decorator + class MyException(Exception): + pass + + @contextmanager + def do_nothing(): + yield + + with do_nothing(), pytest.raises(MyException) as ei: + raise MyException() + + assert isinstance(ei.value, MyException) + + # this should not raise an exception either + ei.value.__traceback__ = ei.value.__traceback__ + def test_converts_and_validates_by_default(self): """ If no on_setattr is set, assume setters.convert, setters.validate. @@ -346,7 +389,6 @@ class TestNextGen: @attrs.define class A: - x: int = 10 def xx(self): diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_packaging.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_packaging.py new file mode 100644 index 00000000000..046ae4c39dd --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_packaging.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: MIT + +import sys + +import pytest + +import attr +import attrs + + +if sys.version_info < (3, 8): + import importlib_metadata as metadata +else: + from importlib import metadata + + +@pytest.fixture(name="mod", params=(attr, attrs)) +def _mod(request): + return request.param + + +class TestLegacyMetadataHack: + def test_title(self, mod): + """ + __title__ returns attrs. + """ + with pytest.deprecated_call() as ws: + assert "attrs" == mod.__title__ + + assert ( + f"Accessing {mod.__name__}.__title__ is deprecated" + in ws.list[0].message.args[0] + ) + + def test_copyright(self, mod): + """ + __copyright__ returns the correct blurp. + """ + with pytest.deprecated_call() as ws: + assert "Copyright (c) 2015 Hynek Schlawack" == mod.__copyright__ + + assert ( + f"Accessing {mod.__name__}.__copyright__ is deprecated" + in ws.list[0].message.args[0] + ) + + def test_version(self, mod, recwarn): + """ + __version__ returns the correct version and doesn't warn. + """ + assert metadata.version("attrs") == mod.__version__ + + assert [] == recwarn.list + + def test_description(self, mod): + """ + __description__ returns the correct description. + """ + with pytest.deprecated_call() as ws: + assert "Classes Without Boilerplate" == mod.__description__ + + assert ( + f"Accessing {mod.__name__}.__description__ is deprecated" + in ws.list[0].message.args[0] + ) + + @pytest.mark.parametrize("name", ["__uri__", "__url__"]) + def test_uri(self, mod, name): + """ + __uri__ & __url__ returns the correct project URL. + """ + with pytest.deprecated_call() as ws: + assert "https://www.attrs.org/" == getattr(mod, name) + + assert ( + f"Accessing {mod.__name__}.{name} is deprecated" + in ws.list[0].message.args[0] + ) + + def test_author(self, mod): + """ + __author__ returns Hynek. + """ + with pytest.deprecated_call() as ws: + assert "Hynek Schlawack" == mod.__author__ + + assert ( + f"Accessing {mod.__name__}.__author__ is deprecated" + in ws.list[0].message.args[0] + ) + + def test_email(self, mod): + """ + __email__ returns Hynek's email address. + """ + with pytest.deprecated_call() as ws: + assert "hs@ox.cx" == mod.__email__ + + assert ( + f"Accessing {mod.__name__}.__email__ is deprecated" + in ws.list[0].message.args[0] + ) + + def test_license(self, mod): + """ + __license__ returns MIT. + """ + with pytest.deprecated_call() as ws: + assert "MIT" == mod.__license__ + + assert ( + f"Accessing {mod.__name__}.__license__ is deprecated" + in ws.list[0].message.args[0] + ) + + def test_does_not_exist(self, mod): + """ + Asking for unsupported dunders raises an AttributeError. + """ + with pytest.raises( + AttributeError, + match=f"module {mod.__name__} has no attribute __yolo__", + ): + mod.__yolo__ + + def test_version_info(self, recwarn, mod): + """ + ___version_info__ is not deprected, therefore doesn't raise a warning + and parses correctly. + """ + assert isinstance(mod.__version_info__, attr.VersionInfo) + assert [] == recwarn.list diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_pattern_matching.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_pattern_matching.py index 590804a8a7a..3855d6a379c 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_pattern_matching.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_pattern_matching.py @@ -19,7 +19,7 @@ class TestPatternMatching: """ @dec - class C(object): + class C: a = attr.ib() assert ("a",) == C.__match_args__ diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_pyright.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_pyright.py index c30dcc5cb16..800d6099fab 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_pyright.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_pyright.py @@ -1,47 +1,55 @@ # SPDX-License-Identifier: MIT +from __future__ import annotations + import json -import os.path import shutil import subprocess -import sys - -import pytest -import attr +from pathlib import Path +import pytest -if sys.version_info < (3, 6): - _found_pyright = False -else: - _found_pyright = shutil.which("pyright") +import attrs -@attr.s(frozen=True) -class PyrightDiagnostic(object): - severity = attr.ib() - message = attr.ib() +pytestmark = [ + pytest.mark.skipif( + shutil.which("pyright") is None, reason="Requires pyright." + ), +] -@pytest.mark.skipif(not _found_pyright, reason="Requires pyright.") -def test_pyright_baseline(): - """The __dataclass_transform__ decorator allows pyright to determine - attrs decorated class types. - """ +@attrs.frozen +class PyrightDiagnostic: + severity: str + message: str - test_file = os.path.dirname(__file__) + "/dataclass_transform_example.py" - pyright = subprocess.run( +def parse_pyright_output(test_file: Path) -> set[PyrightDiagnostic]: + pyright = subprocess.run( # noqa: PLW1510 ["pyright", "--outputjson", str(test_file)], capture_output=True ) + pyright_result = json.loads(pyright.stdout) - diagnostics = set( + return { PyrightDiagnostic(d["severity"], d["message"]) for d in pyright_result["generalDiagnostics"] - ) + } - # Expected diagnostics as per pyright 1.1.135 + +def test_pyright_baseline(): + """ + The typing.dataclass_transform decorator allows pyright to determine + attrs decorated class types. + """ + + test_file = Path(__file__).parent / "dataclass_transform_example.py" + + diagnostics = parse_pyright_output(test_file) + + # Expected diagnostics as per pyright 1.1.311 expected_diagnostics = { PyrightDiagnostic( severity="information", @@ -51,7 +59,13 @@ def test_pyright_baseline(): PyrightDiagnostic( severity="information", message='Type of "DefineConverter.__init__" is ' - '"(self: DefineConverter, with_converter: int) -> None"', + '"(self: DefineConverter, with_converter: str | Buffer | ' + 'SupportsInt | SupportsIndex | SupportsTrunc) -> None"', + ), + PyrightDiagnostic( + severity="error", + message='Cannot assign member "a" for type ' + '"Frozen"\n\xa0\xa0"Frozen" is frozen\n\xa0\xa0\xa0\xa0Member "__set__" is unknown', ), PyrightDiagnostic( severity="information", @@ -60,12 +74,45 @@ def test_pyright_baseline(): PyrightDiagnostic( severity="error", message='Cannot assign member "a" for type ' - '"FrozenDefine"\n\xa0\xa0"FrozenDefine" is frozen', + '"FrozenDefine"\n\xa0\xa0"FrozenDefine" is frozen\n\xa0\xa0\xa0\xa0' + 'Member "__set__" is unknown', ), PyrightDiagnostic( severity="information", message='Type of "d2.a" is "Literal[\'new\']"', ), + PyrightDiagnostic( + severity="information", + message='Type of "af.__init__" is "(_a: int) -> None"', + ), } + assert expected_diagnostics == diagnostics + + +def test_pyright_attrsinstance_compat(tmp_path): + """ + Test that `AttrsInstance` is compatible with Pyright. + """ + test_pyright_attrsinstance_compat_path = ( + tmp_path / "test_pyright_attrsinstance_compat.py" + ) + test_pyright_attrsinstance_compat_path.write_text( + """\ +import attrs + +# We can assign any old object to `AttrsInstance`. +foo: attrs.AttrsInstance = object() + +reveal_type(attrs.AttrsInstance) +""" + ) + + diagnostics = parse_pyright_output(test_pyright_attrsinstance_compat_path) + expected_diagnostics = { + PyrightDiagnostic( + severity="information", + message='Type of "attrs.AttrsInstance" is "type[AttrsInstance]"', + ), + } assert diagnostics == expected_diagnostics diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_setattr.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_setattr.py index aaedde57464..c7b90daee68 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_setattr.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_setattr.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function import pickle @@ -9,22 +8,21 @@ import pytest import attr from attr import setters -from attr._compat import PY2 from attr.exceptions import FrozenAttributeError from attr.validators import instance_of, matches_re @attr.s(frozen=True) -class Frozen(object): +class Frozen: x = attr.ib() @attr.s -class WithOnSetAttrHook(object): +class WithOnSetAttrHook: x = attr.ib(on_setattr=lambda *args: None) -class TestSetAttr(object): +class TestSetAttr: def test_change(self): """ The return value of a hook overwrites the value. But they are not run @@ -35,7 +33,7 @@ class TestSetAttr(object): return "hooked!" @attr.s - class Hooked(object): + class Hooked: x = attr.ib(on_setattr=hook) y = attr.ib() @@ -56,7 +54,7 @@ class TestSetAttr(object): """ @attr.s - class PartiallyFrozen(object): + class PartiallyFrozen: x = attr.ib(on_setattr=setters.frozen) y = attr.ib() @@ -81,7 +79,7 @@ class TestSetAttr(object): """ @attr.s(on_setattr=on_setattr) - class ValidatedAttribute(object): + class ValidatedAttribute: x = attr.ib() y = attr.ib(validator=[instance_of(str), matches_re("foo.*qux")]) @@ -115,7 +113,7 @@ class TestSetAttr(object): s = [setters.convert, lambda _, __, nv: nv + 1] @attr.s - class Piped(object): + class Piped: x1 = attr.ib(converter=int, on_setattr=setters.pipe(*s)) x2 = attr.ib(converter=int, on_setattr=s) @@ -147,7 +145,7 @@ class TestSetAttr(object): """ @attr.s(on_setattr=[setters.convert, setters.validate]) - class C(object): + class C: x = attr.ib() c = C(1) @@ -162,7 +160,7 @@ class TestSetAttr(object): """ @attr.s(on_setattr=setters.validate) - class C(object): + class C: x = attr.ib(validator=attr.validators.instance_of(int)) c = C(1) @@ -187,7 +185,7 @@ class TestSetAttr(object): with pytest.raises(ValueError) as ei: @attr.s(frozen=True, on_setattr=setters.validate) - class C(object): + class C: x = attr.ib() assert "Frozen classes can't use on_setattr." == ei.value.args[0] @@ -200,12 +198,11 @@ class TestSetAttr(object): with pytest.raises(ValueError) as ei: @attr.s(frozen=True) - class C(object): + class C: x = attr.ib(on_setattr=setters.validate) assert "Frozen classes can't use on_setattr." == ei.value.args[0] - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_reset_if_no_custom_setattr(self, slots): """ If a class with an active setattr is subclassed and no new setattr @@ -218,29 +215,26 @@ class TestSetAttr(object): pytest.fail("Must not be called.") @attr.s - class Hooked(object): + class Hooked: x = attr.ib(on_setattr=boom) @attr.s(slots=slots) class NoHook(WithOnSetAttrHook): x = attr.ib() - if not PY2: - assert NoHook.__setattr__ == object.__setattr__ - + assert NoHook.__setattr__ == object.__setattr__ assert 1 == NoHook(1).x assert Hooked.__attrs_own_setattr__ assert not NoHook.__attrs_own_setattr__ assert WithOnSetAttrHook.__attrs_own_setattr__ - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_inherited_do_not_reset(self, slots): """ If we inherit a __setattr__ that has been written by the user, we must not reset it unless necessary. """ - class A(object): + class A: """ Not an attrs class on purpose to prevent accidental resets that would render the asserts meaningless. @@ -261,7 +255,6 @@ class TestSetAttr(object): assert C.__setattr__ == A.__setattr__ - @pytest.mark.parametrize("slots", [True, False]) def test_pickling_retains_attrs_own(self, slots): """ Pickling/Unpickling does not lose ownership information about @@ -288,7 +281,7 @@ class TestSetAttr(object): """ @attr.s(slots=True) - class A(object): + class A: def __setattr__(self, key, value): raise SystemError @@ -306,7 +299,7 @@ class TestSetAttr(object): """ @attr.s(slots=True) - class A(object): + class A: x = attr.ib(on_setattr=setters.frozen) class B(A): @@ -318,14 +311,6 @@ class TestSetAttr(object): C(1).x = 2 - -@pytest.mark.skipif(PY2, reason="Python 3-only.") -class TestSetAttrNoPy2(object): - """ - __setattr__ tests for Py3+ to avoid the skip repetition. - """ - - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_auto_detect_if_no_custom_setattr(self, slots): """ It's possible to remove the on_setattr hook from an attribute and @@ -345,7 +330,6 @@ class TestSetAttrNoPy2(object): assert not RemoveNeedForOurSetAttr.__attrs_own_setattr__ assert 2 == i.x - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_restore_respects_auto_detect(self, slots): """ If __setattr__ should be restored but the user supplied its own and @@ -359,7 +343,6 @@ class TestSetAttrNoPy2(object): assert CustomSetAttr.__setattr__ != object.__setattr__ - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_auto_detect_frozen(self, slots): """ frozen=True together with a detected custom __setattr__ are rejected. @@ -373,7 +356,6 @@ class TestSetAttrNoPy2(object): def __setattr__(self, _, __): pass - @pytest.mark.parametrize("slots", [True, False]) def test_setattr_auto_detect_on_setattr(self, slots): """ on_setattr attributes together with a detected custom __setattr__ are @@ -385,7 +367,7 @@ class TestSetAttrNoPy2(object): ): @attr.s(auto_detect=True, slots=slots) - class HookAndCustomSetAttr(object): + class HookAndCustomSetAttr: x = attr.ib(on_setattr=lambda *args: None) def __setattr__(self, _, __): @@ -401,12 +383,12 @@ class TestSetAttrNoPy2(object): A user-provided intermediate __setattr__ is not reset to object.__setattr__. - This only can work on Python 3+ with auto_detect activated, such that - attrs can know that there is a user-provided __setattr__. + This only can work with auto_detect activated, such that attrs can know + that there is a user-provided __setattr__. """ @attr.s(slots=a_slots) - class A(object): + class A: x = attr.ib(on_setattr=setters.frozen) @attr.s(slots=b_slots, auto_detect=True) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_slots.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_slots.py index baf9a40ddb8..26365ab0d2b 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_slots.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_slots.py @@ -3,17 +3,18 @@ """ Unit tests for slots-related functionality. """ - +import functools import pickle -import sys -import types import weakref +from unittest import mock + import pytest import attr +import attrs -from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell +from attr._compat import PY_3_8_PLUS, PYPY # Pympler doesn't work on PyPy. @@ -21,12 +22,12 @@ try: from pympler.asizeof import asizeof has_pympler = True -except BaseException: # Won't be an import error. +except BaseException: # Won't be an import error. # noqa: BLE001 has_pympler = False @attr.s -class C1(object): +class C1: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @@ -41,18 +42,16 @@ class C1(object): def staticmethod(): return "staticmethod" - if not PY2: + def my_class(self): + return __class__ - def my_class(self): - return __class__ - - def my_super(self): - """Just to test out the no-arg super.""" - return super().__repr__() + def my_super(self): + """Just to test out the no-arg super.""" + return super().__repr__() @attr.s(slots=True, hash=True) -class C1Slots(object): +class C1Slots: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @@ -67,14 +66,12 @@ class C1Slots(object): def staticmethod(): return "staticmethod" - if not PY2: - - def my_class(self): - return __class__ + def my_class(self): + return __class__ - def my_super(self): - """Just to test out the no-arg super.""" - return super().__repr__() + def my_super(self): + """Just to test out the no-arg super.""" + return super().__repr__() def test_slots_being_used(): @@ -90,7 +87,7 @@ def test_slots_being_used(): assert "__dict__" in dir(non_slot_instance) assert "__slots__" not in dir(non_slot_instance) - assert set(["__weakref__", "x", "y"]) == set(slot_instance.__slots__) + assert {"__weakref__", "x", "y"} == set(slot_instance.__slots__) if has_pympler: assert asizeof(slot_instance) < asizeof(non_slot_instance) @@ -154,7 +151,7 @@ def test_inheritance_from_nonslots(): assert "clsmethod" == c2.classmethod() assert "staticmethod" == c2.staticmethod() - assert set(["z"]) == set(C2Slots.__slots__) + assert {"z"} == set(C2Slots.__slots__) c3 = C2Slots(x=1, y=3, z="test") @@ -178,7 +175,7 @@ def test_nonslots_these(): This will actually *replace* the class with another one, using slots. """ - class SimpleOrdinaryClass(object): + class SimpleOrdinaryClass: def __init__(self, x, y, z): self.x = x self.y = y @@ -213,7 +210,7 @@ def test_nonslots_these(): assert "clsmethod" == c2.classmethod() assert "staticmethod" == c2.staticmethod() - assert set(["__weakref__", "x", "y", "z"]) == set(C2Slots.__slots__) + assert {"__weakref__", "x", "y", "z"} == set(C2Slots.__slots__) c3 = C2Slots(x=1, y=3, z="test") assert c3 > c2 @@ -245,7 +242,7 @@ def test_inheritance_from_slots(): assert 2 == c2.y assert "test" == c2.z - assert set(["z"]) == set(C2Slots.__slots__) + assert {"z"} == set(C2Slots.__slots__) assert 1 == c2.method() assert "clsmethod" == c2.classmethod() @@ -275,7 +272,7 @@ def test_inheritance_from_slots_with_attribute_override(): Inheriting from a slotted class doesn't re-create existing slots """ - class HasXSlot(object): + class HasXSlot: __slots__ = ("x",) @attr.s(slots=True, hash=True) @@ -311,7 +308,7 @@ def test_inherited_slot_reuses_slot_descriptor(): We reuse slot descriptor for an attr.ib defined in a slotted attr.s """ - class HasXSlot(object): + class HasXSlot: __slots__ = ("x",) class OverridesX(HasXSlot): @@ -342,7 +339,7 @@ def test_bare_inheritance_from_slots(): @attr.s( init=False, eq=False, order=False, hash=False, repr=False, slots=True ) - class C1BareSlots(object): + class C1BareSlots: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @@ -358,7 +355,7 @@ def test_bare_inheritance_from_slots(): return "staticmethod" @attr.s(init=False, eq=False, order=False, hash=False, repr=False) - class C1Bare(object): + class C1Bare: x = attr.ib(validator=attr.validators.instance_of(int)) y = attr.ib() @@ -409,8 +406,7 @@ def test_bare_inheritance_from_slots(): assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2) -@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.") -class TestClosureCellRewriting(object): +class TestClosureCellRewriting: def test_closure_cell_rewriting(self): """ Slotted classes support proper closure cell rewriting. @@ -457,7 +453,6 @@ class TestClosureCellRewriting(object): assert non_slot_instance.my_subclass() is C2 assert slot_instance.my_subclass() is C2Slots - @pytest.mark.parametrize("slots", [True, False]) def test_cls_static(self, slots): """ Slotted classes support proper closure cell rewriting for class- and @@ -482,36 +477,6 @@ class TestClosureCellRewriting(object): assert D.statmethod() is D - @pytest.mark.skipif(PYPY, reason="set_closure_cell always works on PyPy") - @pytest.mark.skipif( - sys.version_info >= (3, 8), - reason="can't break CodeType.replace() via monkeypatch", - ) - def test_code_hack_failure(self, monkeypatch): - """ - Keeps working if function/code object introspection doesn't work - on this (nonstandard) interpeter. - - A warning is emitted that points to the actual code. - """ - # This is a pretty good approximation of the behavior of - # the actual types.CodeType on Brython. - monkeypatch.setattr(types, "CodeType", lambda: None) - func = make_set_closure_cell() - - with pytest.warns(RuntimeWarning) as wr: - func() - - w = wr.pop() - assert __file__ == w.filename - assert ( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - ) == w.message.args - - assert just_warn is func - @pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython") def test_not_weakrefable(): @@ -520,7 +485,7 @@ def test_not_weakrefable(): """ @attr.s(slots=True, weakref_slot=False) - class C(object): + class C: pass c = C() @@ -538,7 +503,7 @@ def test_implicitly_weakrefable(): """ @attr.s(slots=True, weakref_slot=False) - class C(object): + class C: pass c = C() @@ -553,7 +518,7 @@ def test_weakrefable(): """ @attr.s(slots=True, weakref_slot=True) - class C(object): + class C: pass c = C() @@ -568,7 +533,7 @@ def test_weakref_does_not_add_a_field(): """ @attr.s(slots=True, weakref_slot=True) - class C(object): + class C: field = attr.ib() assert [f.name for f in attr.fields(C)] == ["field"] @@ -581,7 +546,7 @@ def tests_weakref_does_not_add_when_inheriting_with_weakref(): """ @attr.s(slots=True, weakref_slot=True) - class C(object): + class C: pass @attr.s(slots=True, weakref_slot=True) @@ -601,7 +566,7 @@ def tests_weakref_does_not_add_with_weakref_attribute(): """ @attr.s(slots=True, weakref_slot=True) - class C(object): + class C: __weakref__ = attr.ib( init=False, hash=False, repr=False, eq=False, order=False ) @@ -618,8 +583,8 @@ def test_slots_empty_cell(): closure cells are present with no contents in a `slots=True` class. (issue https://github.com/python-attrs/attrs/issues/589) - On Python 3, if a method mentions `__class__` or uses the no-arg `super()`, - the compiler will bake a reference to the class in the method itself as + If a method mentions `__class__` or uses the no-arg `super()`, the compiler + will bake a reference to the class in the method itself as `method.__closure__`. Since `attrs` replaces the class with a clone, `_ClassBuilder._create_slots_class(self)` will rewrite these references so it keeps working. This method was not properly covering the edge case where @@ -628,26 +593,26 @@ def test_slots_empty_cell(): """ @attr.s(slots=True) - class C(object): + class C: field = attr.ib() def f(self, a): - super(C, self).__init__() + super(C, self).__init__() # noqa: UP008 C(field=1) @attr.s(getstate_setstate=True) -class C2(object): +class C2: x = attr.ib() @attr.s(slots=True, getstate_setstate=True) -class C2Slots(object): +class C2Slots: x = attr.ib() -class TestPickle(object): +class TestPickle: @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL)) def test_pickleable_by_default(self, protocol): """ @@ -665,10 +630,12 @@ class TestPickle(object): As long as getstate_setstate is None, nothing is done to dict classes. """ - i = C1(1, 2) - - assert None is getattr(i, "__getstate__", None) - assert None is getattr(i, "__setstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C1, "__getstate__", None + ) + assert getattr(object, "__setstate__", None) is getattr( + C1, "__setstate__", None + ) def test_no_getstate_setstate_if_option_false(self): """ @@ -676,13 +643,15 @@ class TestPickle(object): """ @attr.s(slots=True, getstate_setstate=False) - class C(object): + class C: x = attr.ib() - i = C(42) - - assert None is getattr(i, "__getstate__", None) - assert None is getattr(i, "__setstate__", None) + assert getattr(object, "__getstate__", None) is getattr( + C, "__getstate__", None + ) + assert getattr(object, "__setstate__", None) is getattr( + C, "__setstate__", None + ) @pytest.mark.parametrize("cls", [C2(1), C2Slots(1)]) def test_getstate_set_state_force_true(self, cls): @@ -695,11 +664,11 @@ class TestPickle(object): def test_slots_super_property_get(): """ - On Python 2/3: the `super(self.__class__, self)` works. + Both `super()` and `super(self.__class__, self)` work. """ @attr.s(slots=True) - class A(object): + class A: x = attr.ib() @property @@ -710,20 +679,27 @@ def test_slots_super_property_get(): class B(A): @property def f(self): - return super(B, self).f ** 2 + return super().f ** 2 + + @attr.s(slots=True) + class C(A): + @property + def f(self): + return super(C, self).f ** 2 # noqa: UP008 assert B(11).f == 121 assert B(17).f == 289 + assert C(11).f == 121 + assert C(17).f == 289 -@pytest.mark.skipif(PY2, reason="shortcut super() is PY3-only.") -def test_slots_super_property_get_shurtcut(): +def test_slots_super_property_get_shortcut(): """ - On Python 3, the `super()` shortcut is allowed. + The `super()` shortcut is allowed. """ @attr.s(slots=True) - class A(object): + class A: x = attr.ib() @property @@ -738,3 +714,438 @@ def test_slots_super_property_get_shurtcut(): assert B(11).f == 121 assert B(17).f == 289 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_allows_call(): + """ + cached_property in slotted class allows call. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + assert A(11).f == 11 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_class_does_not_have__dict__(): + """ + slotted class with cached property has no __dict__ attribute. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + assert set(A.__slots__) == {"x", "f", "__weakref__"} + assert "__dict__" not in dir(A) + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_works_on_frozen_isntances(): + """ + Infers type of cached property. + """ + + @attrs.frozen(slots=True) + class A: + x: int + + @functools.cached_property + def f(self) -> int: + return self.x + + assert A(x=1).f == 1 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_infers_type(): + """ + Infers type of cached property. + """ + + @attrs.frozen(slots=True) + class A: + x: int + + @functools.cached_property + def f(self) -> int: + return self.x + + assert A.__annotations__ == {"x": int, "f": int} + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_with_empty_getattr_raises_attribute_error_of_requested(): + """ + Ensures error information is not lost. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + a = A(1) + with pytest.raises( + AttributeError, match="'A' object has no attribute 'z'" + ): + a.z + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_with_getattr_calls_getattr_for_missing_attributes(): + """ + Ensure __getattr__ implementation is maintained for non cached_properties. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + def __getattr__(self, item): + return item + + a = A(1) + assert a.f == 1 + assert a.z == "z" + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_getattr_in_superclass__is_called_for_missing_attributes_when_cached_property_present(): + """ + Ensure __getattr__ implementation is maintained in subclass. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + def __getattr__(self, item): + return item + + @attr.s(slots=True) + class B(A): + @functools.cached_property + def f(self): + return self.x + + b = B(1) + assert b.f == 1 + assert b.z == "z" + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_getattr_in_subclass_gets_superclass_cached_property(): + """ + Ensure super() in __getattr__ is not broken through cached_property re-write. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + def __getattr__(self, item): + return item + + @attr.s(slots=True) + class B(A): + @functools.cached_property + def g(self): + return self.x + + def __getattr__(self, item): + return super().__getattr__(item) + + b = B(1) + assert b.f == 1 + assert b.z == "z" + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_sub_class_with_independent_cached_properties_both_work(): + """ + Subclassing shouldn't break cached properties. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + @attr.s(slots=True) + class B(A): + @functools.cached_property + def g(self): + return self.x * 2 + + assert B(1).f == 1 + assert B(1).g == 2 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_with_multiple_cached_property_subclasses_works(): + """ + Multiple sub-classes shouldn't break cached properties. + """ + + @attr.s(slots=True) + class A: + x = attr.ib(kw_only=True) + + @functools.cached_property + def f(self): + return self.x + + @attr.s(slots=False) + class B: + @functools.cached_property + def g(self): + return self.x * 2 + + def __getattr__(self, item): + if hasattr(super(), "__getattr__"): + return super().__getattr__(item) + return item + + @attr.s(slots=True) + class AB(A, B): + pass + + ab = AB(x=1) + + assert ab.f == 1 + assert ab.g == 2 + assert ab.h == "h" + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_sub_class_avoids_duplicated_slots(): + """ + Duplicating the slots is a waste of memory. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + @attr.s(slots=True) + class B(A): + @functools.cached_property + def f(self): + return self.x * 2 + + assert B(1).f == 2 + assert B.__slots__ == () + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_sub_class_with_actual_slot(): + """ + A sub-class can have an explicit attrs field that replaces a cached property. + """ + + @attr.s(slots=True) + class A: # slots : (x, f) + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + @attr.s(slots=True) + class B(A): + f: int = attr.ib() + + assert B(1, 2).f == 2 + assert B.__slots__ == () + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_is_not_called_at_construction(): + """ + A cached property function should only be called at property access point. + """ + call_count = 0 + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + nonlocal call_count + call_count += 1 + return self.x + + A(1) + assert call_count == 0 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_repeat_call_only_once(): + """ + A cached property function should be called only once, on repeated attribute access. + """ + call_count = 0 + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + nonlocal call_count + call_count += 1 + return self.x + + obj = A(1) + obj.f + obj.f + assert call_count == 1 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_property_called_independent_across_instances(): + """ + A cached property value should be specific to the given instance. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f(self): + return self.x + + obj_1 = A(1) + obj_2 = A(2) + + assert obj_1.f == 1 + assert obj_2.f == 2 + + +@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+") +def test_slots_cached_properties_work_independently(): + """ + Multiple cached properties should work independently. + """ + + @attr.s(slots=True) + class A: + x = attr.ib() + + @functools.cached_property + def f_1(self): + return self.x + + @functools.cached_property + def f_2(self): + return self.x * 2 + + obj = A(1) + + assert obj.f_1 == 1 + assert obj.f_2 == 2 + + +@attr.s(slots=True) +class A: + x = attr.ib() + b = attr.ib() + c = attr.ib() + + +def test_slots_unpickle_after_attr_removed(): + """ + We don't assign attributes we don't have anymore if the class has + removed it. + """ + a = A(1, 2, 3) + a_pickled = pickle.dumps(a) + a_unpickled = pickle.loads(a_pickled) + assert a_unpickled == a + + @attr.s(slots=True) + class NEW_A: + x = attr.ib() + c = attr.ib() + + with mock.patch(f"{__name__}.A", NEW_A): + new_a = pickle.loads(a_pickled) + + assert new_a.x == 1 + assert new_a.c == 3 + assert not hasattr(new_a, "b") + + +def test_slots_unpickle_after_attr_added(frozen): + """ + We don't assign attribute we haven't had before if the class has one added. + """ + a = A(1, 2, 3) + a_pickled = pickle.dumps(a) + a_unpickled = pickle.loads(a_pickled) + + assert a_unpickled == a + + @attr.s(slots=True, frozen=frozen) + class NEW_A: + x = attr.ib() + b = attr.ib() + d = attr.ib() + c = attr.ib() + + with mock.patch(f"{__name__}.A", NEW_A): + new_a = pickle.loads(a_pickled) + + assert new_a.x == 1 + assert new_a.b == 2 + assert new_a.c == 3 + assert not hasattr(new_a, "d") + + +def test_slots_unpickle_is_backward_compatible(frozen): + """ + Ensure object pickled before v22.2.0 can still be unpickled. + """ + a = A(1, 2, 3) + + a_pickled = ( + b"\x80\x04\x95&\x00\x00\x00\x00\x00\x00\x00\x8c\x10" + + a.__module__.encode() + + b"\x94\x8c\x01A\x94\x93\x94)\x81\x94K\x01K\x02K\x03\x87\x94b." + ) + + a_unpickled = pickle.loads(a_pickled) + + assert a_unpickled == a diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_utils.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_utils.py new file mode 100644 index 00000000000..92c04a1b503 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_utils.py @@ -0,0 +1,19 @@ +from .utils import simple_class + + +class TestSimpleClass: + """ + Tests for the testing helper function `make_class`. + """ + + def test_returns_class(self): + """ + Returns a class object. + """ + assert type is simple_class().__class__ + + def test_returns_distinct_classes(self): + """ + Each call returns a completely new class. + """ + assert simple_class() is not simple_class() diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_validators.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_validators.py index d7c6de8bad7..4327f825188 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_validators.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_validators.py @@ -4,7 +4,6 @@ Tests for `attr.validators`. """ -from __future__ import absolute_import, division, print_function import re @@ -14,8 +13,8 @@ import attr from attr import _config, fields, has from attr import validators as validator_module -from attr._compat import PY2, TYPE from attr.validators import ( + _subclass_of, and_, deep_iterable, deep_mapping, @@ -28,6 +27,8 @@ from attr.validators import ( lt, matches_re, max_len, + min_len, + not_, optional, provides, ) @@ -48,9 +49,9 @@ def zope_interface(): return zope.interface -class TestDisableValidators(object): +class TestDisableValidators: @pytest.fixture(autouse=True) - def reset_default(self): + def _reset_default(self): """ Make sure validators are always enabled after a test. """ @@ -63,8 +64,10 @@ class TestDisableValidators(object): """ assert _config._run_validators is True - @pytest.mark.parametrize("value, expected", [(True, False), (False, True)]) - def test_set_validators_diabled(self, value, expected): + @pytest.mark.parametrize( + ("value", "expected"), [(True, False), (False, True)] + ) + def test_set_validators_disabled(self, value, expected): """ Sets `_run_validators`. """ @@ -72,7 +75,9 @@ class TestDisableValidators(object): assert _config._run_validators is expected - @pytest.mark.parametrize("value, expected", [(True, False), (False, True)]) + @pytest.mark.parametrize( + ("value", "expected"), [(True, False), (False, True)] + ) def test_disabled(self, value, expected): """ Returns `_run_validators`. @@ -99,16 +104,15 @@ class TestDisableValidators(object): """ assert _config._run_validators is True - with pytest.raises(ValueError): - with validator_module.disabled(): - assert _config._run_validators is False + with pytest.raises(ValueError), validator_module.disabled(): + assert _config._run_validators is False - raise ValueError("haha!") + raise ValueError("haha!") assert _config._run_validators is True -class TestInstanceOf(object): +class TestInstanceOf: """ Tests for `instance_of`. """ @@ -143,8 +147,7 @@ class TestInstanceOf(object): with pytest.raises(TypeError) as e: v(None, a, "42") assert ( - "'test' must be <{type} 'int'> (got '42' that is a <{type} " - "'str'>).".format(type=TYPE), + "'test' must be <class 'int'> (got '42' that is a <class 'str'>).", a, int, "42", @@ -155,12 +158,10 @@ class TestInstanceOf(object): Returned validator has a useful `__repr__`. """ v = instance_of(int) - assert ( - "<instance_of validator for type <{type} 'int'>>".format(type=TYPE) - ) == repr(v) + assert ("<instance_of validator for type <class 'int'>>") == repr(v) -class TestMatchesRe(object): +class TestMatchesRe: """ Tests for `matches_re`. """ @@ -177,7 +178,7 @@ class TestMatchesRe(object): """ @attr.s - class ReTester(object): + class ReTester: str_match = attr.ib(validator=matches_re("a|ab")) ReTester("ab") # shouldn't raise exceptions @@ -194,7 +195,7 @@ class TestMatchesRe(object): """ @attr.s - class MatchTester(object): + class MatchTester: val = attr.ib(validator=matches_re("a", re.IGNORECASE, re.match)) MatchTester("A1") # test flags and using re.match @@ -206,7 +207,7 @@ class TestMatchesRe(object): pattern = re.compile("a") @attr.s - class RePatternTester(object): + class RePatternTester: val = attr.ib(validator=matches_re(pattern)) RePatternTester("a") @@ -228,7 +229,7 @@ class TestMatchesRe(object): """ @attr.s - class SearchTester(object): + class SearchTester: val = attr.ib(validator=matches_re("a", 0, re.search)) SearchTester("bab") # re.search will match @@ -240,16 +241,10 @@ class TestMatchesRe(object): with pytest.raises(ValueError) as ei: matches_re("a", 0, lambda: None) - if not PY2: - assert ( - "'func' must be one of None, fullmatch, match, search." - == ei.value.args[0] - ) - else: - assert ( - "'func' must be one of None, match, search." - == ei.value.args[0] - ) + assert ( + "'func' must be one of None, fullmatch, match, search." + == ei.value.args[0] + ) @pytest.mark.parametrize( "func", [None, getattr(re, "fullmatch", None), re.match, re.search] @@ -282,7 +277,7 @@ def always_fail(_, __, ___): 0 / 0 -class TestAnd(object): +class TestAnd: def test_in_all(self): """ Verify that this validator is in ``__all__``. @@ -312,7 +307,7 @@ class TestAnd(object): """ @attr.s - class C(object): + class C: a1 = attr.ib("a1", validator=and_(instance_of(int))) a2 = attr.ib("a2", validator=[instance_of(int)]) @@ -336,7 +331,7 @@ def ifoo(zope_interface): return IFoo -class TestProvides(object): +class TestProvides: """ Tests for `provides`. """ @@ -353,11 +348,13 @@ class TestProvides(object): """ @zope_interface.implementer(ifoo) - class C(object): + class C: def f(self): pass - v = provides(ifoo) + with pytest.deprecated_call(): + v = provides(ifoo) + v(None, simple_attr("x"), C()) def test_fail(self, ifoo): @@ -367,13 +364,14 @@ class TestProvides(object): value = object() a = simple_attr("x") - v = provides(ifoo) + with pytest.deprecated_call(): + v = provides(ifoo) + with pytest.raises(TypeError) as e: v(None, a, value) + assert ( - "'x' must provide {interface!r} which {value!r} doesn't.".format( - interface=ifoo, value=value - ), + f"'x' must provide {ifoo!r} which {value!r} doesn't.", a, ifoo, value, @@ -383,18 +381,21 @@ class TestProvides(object): """ Returned validator has a useful `__repr__`. """ - v = provides(ifoo) - assert ( - "<provides validator for interface {interface!r}>".format( - interface=ifoo - ) - ) == repr(v) + with pytest.deprecated_call(): + v = provides(ifoo) + + assert (f"<provides validator for interface {ifoo!r}>") == repr(v) @pytest.mark.parametrize( - "validator", [instance_of(int), [always_pass, instance_of(int)]] + "validator", + [ + instance_of(int), + [always_pass, instance_of(int)], + (always_pass, instance_of(int)), + ], ) -class TestOptional(object): +class TestOptional: """ Tests for `optional`. """ @@ -428,8 +429,7 @@ class TestOptional(object): with pytest.raises(TypeError) as e: v(None, a, "42") assert ( - "'test' must be <{type} 'int'> (got '42' that is a <{type} " - "'str'>).".format(type=TYPE), + "'test' must be <class 'int'> (got '42' that is a <class 'str'>).", a, int, "42", @@ -444,18 +444,23 @@ class TestOptional(object): if isinstance(validator, list): repr_s = ( "<optional validator for _AndValidator(_validators=[{func}, " - "<instance_of validator for type <{type} 'int'>>]) or None>" - ).format(func=repr(always_pass), type=TYPE) + "<instance_of validator for type <class 'int'>>]) or None>" + ).format(func=repr(always_pass)) + elif isinstance(validator, tuple): + repr_s = ( + "<optional validator for _AndValidator(_validators=({func}, " + "<instance_of validator for type <class 'int'>>)) or None>" + ).format(func=repr(always_pass)) else: repr_s = ( "<optional validator for <instance_of validator for type " - "<{type} 'int'>> or None>" - ).format(type=TYPE) + "<class 'int'>> or None>" + ) assert repr_s == repr(v) -class TestIn_(object): +class TestIn_: """ Tests for `in_`. """ @@ -480,9 +485,16 @@ class TestIn_(object): """ v = in_([1, 2, 3]) a = simple_attr("test") + with pytest.raises(ValueError) as e: v(None, a, None) - assert ("'test' must be in [1, 2, 3] (got None)",) == e.value.args + + assert ( + "'test' must be in [1, 2, 3] (got None)", + a, + [1, 2, 3], + None, + ) == e.value.args def test_fail_with_string(self): """ @@ -493,17 +505,38 @@ class TestIn_(object): a = simple_attr("test") with pytest.raises(ValueError) as e: v(None, a, None) - assert ("'test' must be in 'abc' (got None)",) == e.value.args + assert ( + "'test' must be in 'abc' (got None)", + a, + "abc", + None, + ) == e.value.args def test_repr(self): """ Returned validator has a useful `__repr__`. """ v = in_([3, 4, 5]) - assert (("<in_ validator with options [3, 4, 5]>")) == repr(v) + assert ("<in_ validator with options [3, 4, 5]>") == repr(v) -class TestDeepIterable(object): +@pytest.fixture( + name="member_validator", + params=( + instance_of(int), + [always_pass, instance_of(int)], + (always_pass, instance_of(int)), + ), + scope="module", +) +def _member_validator(request): + """ + Provides sample `member_validator`s for some tests in `TestDeepIterable` + """ + return request.param + + +class TestDeepIterable: """ Tests for `deep_iterable`. """ @@ -514,34 +547,34 @@ class TestDeepIterable(object): """ assert deep_iterable.__name__ in validator_module.__all__ - def test_success_member_only(self): + def test_success_member_only(self, member_validator): """ If the member validator succeeds and the iterable validator is not set, nothing happens. """ - member_validator = instance_of(int) v = deep_iterable(member_validator) a = simple_attr("test") v(None, a, [42]) - def test_success_member_and_iterable(self): + def test_success_member_and_iterable(self, member_validator): """ If both the member and iterable validators succeed, nothing happens. """ - member_validator = instance_of(int) iterable_validator = instance_of(list) v = deep_iterable(member_validator, iterable_validator) a = simple_attr("test") v(None, a, [42]) @pytest.mark.parametrize( - "member_validator, iterable_validator", - ( + ("member_validator", "iterable_validator"), + [ (instance_of(int), 42), (42, instance_of(list)), (42, 42), (42, None), - ), + ([instance_of(int), 42], 42), + ([42, instance_of(int)], 42), + ], ) def test_noncallable_validators( self, member_validator, iterable_validator @@ -552,8 +585,8 @@ class TestDeepIterable(object): with pytest.raises(TypeError) as e: deep_iterable(member_validator, iterable_validator) value = 42 - message = "must be callable (got {value} that is a {type_}).".format( - value=value, type_=value.__class__ + message = ( + f"must be callable (got {value} that is a {value.__class__})." ) assert message in e.value.args[0] @@ -561,17 +594,16 @@ class TestDeepIterable(object): assert message in e.value.msg assert value == e.value.value - def test_fail_invalid_member(self): + def test_fail_invalid_member(self, member_validator): """ Raise member validator error if an invalid member is found. """ - member_validator = instance_of(int) v = deep_iterable(member_validator) a = simple_attr("test") with pytest.raises(TypeError): v(None, a, [42, "42"]) - def test_fail_invalid_iterable(self): + def test_fail_invalid_iterable(self, member_validator): """ Raise iterable validator error if an invalid iterable is found. """ @@ -582,12 +614,11 @@ class TestDeepIterable(object): with pytest.raises(TypeError): v(None, a, [42]) - def test_fail_invalid_member_and_iterable(self): + def test_fail_invalid_member_and_iterable(self, member_validator): """ Raise iterable validator error if both the iterable and a member are invalid. """ - member_validator = instance_of(int) iterable_validator = instance_of(tuple) v = deep_iterable(member_validator, iterable_validator) a = simple_attr("test") @@ -600,14 +631,29 @@ class TestDeepIterable(object): when only member validator is set. """ member_validator = instance_of(int) - member_repr = "<instance_of validator for type <{type} 'int'>>".format( - type=TYPE + member_repr = "<instance_of validator for type <class 'int'>>" + v = deep_iterable(member_validator) + expected_repr = ( + f"<deep_iterable validator for iterables of {member_repr}>" + ) + assert expected_repr == repr(v) + + def test_repr_member_only_sequence(self): + """ + Returned validator has a useful `__repr__` + when only member validator is set and the member validator is a list of + validators + """ + member_validator = [always_pass, instance_of(int)] + member_repr = ( + f"_AndValidator(_validators=({always_pass!r}, " + "<instance_of validator for type <class 'int'>>))" ) v = deep_iterable(member_validator) expected_repr = ( - "<deep_iterable validator for iterables of {member_repr}>" - ).format(member_repr=member_repr) - assert ((expected_repr)) == repr(v) + f"<deep_iterable validator for iterables of {member_repr}>" + ) + assert expected_repr == repr(v) def test_repr_member_and_iterable(self): """ @@ -615,22 +661,39 @@ class TestDeepIterable(object): and iterable validators are set. """ member_validator = instance_of(int) - member_repr = "<instance_of validator for type <{type} 'int'>>".format( - type=TYPE + member_repr = "<instance_of validator for type <class 'int'>>" + iterable_validator = instance_of(list) + iterable_repr = "<instance_of validator for type <class 'list'>>" + v = deep_iterable(member_validator, iterable_validator) + expected_repr = ( + "<deep_iterable validator for" + f" {iterable_repr} iterables of {member_repr}>" + ) + assert expected_repr == repr(v) + + def test_repr_sequence_member_and_iterable(self): + """ + Returned validator has a useful `__repr__` when both member + and iterable validators are set and the member validator is a list of + validators + """ + member_validator = [always_pass, instance_of(int)] + member_repr = ( + f"_AndValidator(_validators=({always_pass!r}, " + "<instance_of validator for type <class 'int'>>))" ) iterable_validator = instance_of(list) - iterable_repr = ( - "<instance_of validator for type <{type} 'list'>>" - ).format(type=TYPE) + iterable_repr = "<instance_of validator for type <class 'list'>>" v = deep_iterable(member_validator, iterable_validator) expected_repr = ( "<deep_iterable validator for" - " {iterable_repr} iterables of {member_repr}>" - ).format(iterable_repr=iterable_repr, member_repr=member_repr) + f" {iterable_repr} iterables of {member_repr}>" + ) + assert expected_repr == repr(v) -class TestDeepMapping(object): +class TestDeepMapping: """ Tests for `deep_mapping`. """ @@ -652,14 +715,14 @@ class TestDeepMapping(object): v(None, a, {"a": 6, "b": 7}) @pytest.mark.parametrize( - "key_validator, value_validator, mapping_validator", - ( + ("key_validator", "value_validator", "mapping_validator"), + [ (42, instance_of(int), None), (instance_of(str), 42, None), (instance_of(str), instance_of(int), 42), (42, 42, None), (42, 42, 42), - ), + ], ) def test_noncallable_validators( self, key_validator, value_validator, mapping_validator @@ -671,8 +734,8 @@ class TestDeepMapping(object): deep_mapping(key_validator, value_validator, mapping_validator) value = 42 - message = "must be callable (got {value} that is a {type_}).".format( - value=value, type_=value.__class__ + message = ( + f"must be callable (got {value} that is a {value.__class__})." ) assert message in e.value.args[0] @@ -719,22 +782,18 @@ class TestDeepMapping(object): Returned validator has a useful `__repr__`. """ key_validator = instance_of(str) - key_repr = "<instance_of validator for type <{type} 'str'>>".format( - type=TYPE - ) + key_repr = "<instance_of validator for type <class 'str'>>" value_validator = instance_of(int) - value_repr = "<instance_of validator for type <{type} 'int'>>".format( - type=TYPE - ) + value_repr = "<instance_of validator for type <class 'int'>>" v = deep_mapping(key_validator, value_validator) expected_repr = ( "<deep_mapping validator for objects mapping " - "{key_repr} to {value_repr}>" - ).format(key_repr=key_repr, value_repr=value_repr) + f"{key_repr} to {value_repr}>" + ) assert expected_repr == repr(v) -class TestIsCallable(object): +class TestIsCallable: """ Tests for `is_callable`. """ @@ -803,7 +862,7 @@ def test_hashability(): class TestLtLeGeGt: """ - Tests for `max_len`. + Tests for `Lt, Le, Ge, Gt`. """ BOUND = 4 @@ -824,13 +883,13 @@ class TestLtLeGeGt: """ @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=v(self.BOUND)) assert fields(Tester).value.validator.bound == self.BOUND @pytest.mark.parametrize( - "v, value", + ("v", "value"), [ (lt, 3), (le, 3), @@ -844,13 +903,13 @@ class TestLtLeGeGt: """Silent if value {op} bound.""" @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=v(self.BOUND)) Tester(value) # shouldn't raise exceptions @pytest.mark.parametrize( - "v, value", + ("v", "value"), [ (lt, 4), (le, 5), @@ -862,7 +921,7 @@ class TestLtLeGeGt: """Raise ValueError if value {op} bound.""" @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=v(self.BOUND)) with pytest.raises(ValueError): @@ -874,9 +933,7 @@ class TestLtLeGeGt: __repr__ is meaningful. """ nv = v(23) - assert repr(nv) == "<Validator for x {op} {bound}>".format( - op=nv.compare_op, bound=23 - ) + assert repr(nv) == f"<Validator for x {nv.compare_op} {23}>" class TestMaxLen: @@ -898,7 +955,7 @@ class TestMaxLen: """ @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=max_len(self.MAX_LENGTH)) assert fields(Tester).value.validator.max_length == self.MAX_LENGTH @@ -921,7 +978,7 @@ class TestMaxLen: """ @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=max_len(self.MAX_LENGTH)) Tester(value) # shouldn't raise exceptions @@ -939,7 +996,7 @@ class TestMaxLen: """ @attr.s - class Tester(object): + class Tester: value = attr.ib(validator=max_len(self.MAX_LENGTH)) with pytest.raises(ValueError): @@ -950,3 +1007,344 @@ class TestMaxLen: __repr__ is meaningful. """ assert repr(max_len(23)) == "<max_len validator for 23>" + + +class TestMinLen: + """ + Tests for `min_len`. + """ + + MIN_LENGTH = 2 + + def test_in_all(self): + """ + validator is in ``__all__``. + """ + assert min_len.__name__ in validator_module.__all__ + + def test_retrieve_min_len(self): + """ + The configured min. length can be extracted from the Attribute + """ + + @attr.s + class Tester: + value = attr.ib(validator=min_len(self.MIN_LENGTH)) + + assert fields(Tester).value.validator.min_length == self.MIN_LENGTH + + @pytest.mark.parametrize( + "value", + [ + "foo", + "spam", + list(range(MIN_LENGTH)), + {"spam": 3, "eggs": 4}, + ], + ) + def test_check_valid(self, value): + """ + Silent if len(value) => min_len. + Values can be strings and other iterables. + """ + + @attr.s + class Tester: + value = attr.ib(validator=min_len(self.MIN_LENGTH)) + + Tester(value) # shouldn't raise exceptions + + @pytest.mark.parametrize( + "value", + [ + "", + list(range(1)), + ], + ) + def test_check_invalid(self, value): + """ + Raise ValueError if len(value) < min_len. + """ + + @attr.s + class Tester: + value = attr.ib(validator=min_len(self.MIN_LENGTH)) + + with pytest.raises(ValueError): + Tester(value) + + def test_repr(self): + """ + __repr__ is meaningful. + """ + assert repr(min_len(23)) == "<min_len validator for 23>" + + +class TestSubclassOf: + """ + Tests for `_subclass_of`. + """ + + def test_success(self): + """ + Nothing happens if classes match. + """ + v = _subclass_of(int) + v(None, simple_attr("test"), int) + + def test_subclass(self): + """ + Subclasses are accepted too. + """ + v = _subclass_of(int) + # yep, bools are a subclass of int :( + v(None, simple_attr("test"), bool) + + def test_fail(self): + """ + Raises `TypeError` on wrong types. + """ + v = _subclass_of(int) + a = simple_attr("test") + with pytest.raises(TypeError) as e: + v(None, a, str) + assert ( + "'test' must be a subclass of <class 'int'> (got <class 'str'>).", + a, + int, + str, + ) == e.value.args + + def test_repr(self): + """ + Returned validator has a useful `__repr__`. + """ + v = _subclass_of(int) + assert ("<subclass_of validator for type <class 'int'>>") == repr(v) + + +class TestNot_: + """ + Tests for `not_`. + """ + + DEFAULT_EXC_TYPES = (ValueError, TypeError) + + def test_not_all(self): + """ + The validator is in ``__all__``. + """ + assert not_.__name__ in validator_module.__all__ + + def test_repr(self): + """ + Returned validator has a useful `__repr__`. + """ + wrapped = in_([3, 4, 5]) + + v = not_(wrapped) + + assert ( + f"<not_ validator wrapping {wrapped!r}, " + f"capturing {v.exc_types!r}>" + ) == repr(v) + + def test_success_because_fails(self): + """ + If the wrapped validator fails, we're happy. + """ + + def always_fails(inst, attr, value): + raise ValueError("always fails") + + v = not_(always_fails) + a = simple_attr("test") + input_value = 3 + + v(1, a, input_value) + + def test_fails_because_success(self): + """ + If the wrapped validator doesn't fail, not_ should fail. + """ + + def always_passes(inst, attr, value): + pass + + v = not_(always_passes) + a = simple_attr("test") + input_value = 3 + + with pytest.raises(ValueError) as e: + v(1, a, input_value) + + assert ( + ( + "not_ validator child '{!r}' did not raise a captured error" + ).format(always_passes), + a, + always_passes, + input_value, + self.DEFAULT_EXC_TYPES, + ) == e.value.args + + def test_composable_with_in_pass(self): + """ + Check something is ``not in`` something else. + """ + v = not_(in_("abc")) + a = simple_attr("test") + input_value = "d" + + v(None, a, input_value) + + def test_composable_with_in_fail(self): + """ + Check something is ``not in`` something else, but it is, so fail. + """ + wrapped = in_("abc") + v = not_(wrapped) + a = simple_attr("test") + input_value = "b" + + with pytest.raises(ValueError) as e: + v(None, a, input_value) + + assert ( + ( + "not_ validator child '{!r}' did not raise a captured error" + ).format(in_("abc")), + a, + wrapped, + input_value, + self.DEFAULT_EXC_TYPES, + ) == e.value.args + + def test_composable_with_matches_re_pass(self): + """ + Check something does not match a regex. + """ + v = not_(matches_re("[a-z]{3}")) + a = simple_attr("test") + input_value = "spam" + + v(None, a, input_value) + + def test_composable_with_matches_re_fail(self): + """ + Check something does not match a regex, but it does, so fail. + """ + wrapped = matches_re("[a-z]{3}") + v = not_(wrapped) + a = simple_attr("test") + input_value = "egg" + + with pytest.raises(ValueError) as e: + v(None, a, input_value) + + assert ( + ( + f"not_ validator child '{wrapped!r}' did not raise a captured error" + ), + a, + wrapped, + input_value, + self.DEFAULT_EXC_TYPES, + ) == e.value.args + + def test_composable_with_instance_of_pass(self): + """ + Check something is not a type. This validator raises a TypeError, + rather than a ValueError like the others. + """ + v = not_(instance_of((int, float))) + a = simple_attr("test") + + v(None, a, "spam") + + def test_composable_with_instance_of_fail(self): + """ + Check something is not a type, but it is, so fail. + """ + wrapped = instance_of((int, float)) + v = not_(wrapped) + a = simple_attr("test") + input_value = 2.718281828 + + with pytest.raises(ValueError) as e: + v(None, a, input_value) + + assert ( + ( + "not_ validator child '{!r}' did not raise a captured error" + ).format(instance_of((int, float))), + a, + wrapped, + input_value, + self.DEFAULT_EXC_TYPES, + ) == e.value.args + + def test_custom_capture_match(self): + """ + Match a custom exception provided to `not_` + """ + v = not_(in_("abc"), exc_types=ValueError) + a = simple_attr("test") + + v(None, a, "d") + + def test_custom_capture_miss(self): + """ + If the exception doesn't match, the underlying raise comes through + """ + + class MyError(Exception): + """:(""" + + wrapped = in_("abc") + v = not_(wrapped, exc_types=MyError) + a = simple_attr("test") + input_value = "d" + + with pytest.raises(ValueError) as e: + v(None, a, input_value) + + # get the underlying exception to compare + with pytest.raises(Exception) as e_from_wrapped: + wrapped(None, a, input_value) + assert e_from_wrapped.value.args == e.value.args + + def test_custom_msg(self): + """ + If provided, use the custom message in the raised error + """ + custom_msg = "custom message!" + wrapped = in_("abc") + v = not_(wrapped, msg=custom_msg) + a = simple_attr("test") + input_value = "a" + + with pytest.raises(ValueError) as e: + v(None, a, input_value) + + assert ( + custom_msg, + a, + wrapped, + input_value, + self.DEFAULT_EXC_TYPES, + ) == e.value.args + + def test_bad_exception_args(self): + """ + Malformed exception arguments + """ + wrapped = in_("abc") + + with pytest.raises(TypeError) as e: + not_(wrapped, exc_types=(str, int)) + + assert ( + "'exc_types' must be a subclass of <class 'Exception'> " + "(got <class 'str'>)." + ) == e.value.args[0] diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/test_version_info.py b/tests/wpt/tests/tools/third_party/attrs/tests/test_version_info.py index 41f75f47a6d..5bd101bcce7 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/test_version_info.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/test_version_info.py @@ -1,11 +1,9 @@ # SPDX-License-Identifier: MIT -from __future__ import absolute_import, division, print_function import pytest from attr import VersionInfo -from attr._compat import PY2 @pytest.fixture(name="vi") @@ -29,9 +27,6 @@ class TestVersionInfo: == VersionInfo._from_version_string("19.2.0.dev0").releaselevel ) - @pytest.mark.skipif( - PY2, reason="Python 2 is too YOLO to care about comparability." - ) @pytest.mark.parametrize("other", [(), (19, 2, 0, "final", "garbage")]) def test_wrong_len(self, vi, other): """ diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/typing_example.py b/tests/wpt/tests/tools/third_party/attrs/tests/typing_example.py index a85c768c104..2124912c8d5 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/typing_example.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/typing_example.py @@ -1,8 +1,10 @@ # SPDX-License-Identifier: MIT +from __future__ import annotations + import re -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Tuple import attr import attrs @@ -49,12 +51,12 @@ CC(a=1) @attr.s class DD: - x: List[int] = attr.ib() + x: list[int] = attr.ib() @attr.s class EE: - y: "List[int]" = attr.ib() + y: "list[int]" = attr.ib() @attr.s @@ -119,6 +121,17 @@ except Error as e: e.args str(e) +# Field aliases + + +@attrs.define +class AliasExample: + without_alias: int + _with_alias: int = attr.ib(alias="_with_alias") + + +attr.fields(AliasExample).without_alias.alias +attr.fields(AliasExample)._with_alias.alias # Converters # XXX: Currently converters can only be functions so none of this works @@ -165,7 +178,7 @@ class Validated: attr.validators.instance_of(C), attr.validators.instance_of(list) ), ) - a = attr.ib( + aa = attr.ib( type=Tuple[C], validator=attr.validators.deep_iterable( attr.validators.instance_of(C), attr.validators.instance_of(tuple) @@ -199,13 +212,40 @@ class Validated: # Test different forms of instance_of g: int = attr.ib(validator=attr.validators.instance_of(int)) h: int = attr.ib(validator=attr.validators.instance_of((int,))) - j: Union[int, str] = attr.ib( - validator=attr.validators.instance_of((int, str)) - ) - k: Union[int, str, C] = attr.ib( + j: int | str = attr.ib(validator=attr.validators.instance_of((int, str))) + k: int | str | C = attr.ib( validator=attrs.validators.instance_of((int, C, str)) ) + l: Any = attr.ib( + validator=attr.validators.not_(attr.validators.in_("abc")) + ) + m: Any = attr.ib( + validator=attr.validators.not_( + attr.validators.in_("abc"), exc_types=ValueError + ) + ) + n: Any = attr.ib( + validator=attr.validators.not_( + attr.validators.in_("abc"), exc_types=(ValueError,) + ) + ) + o: Any = attr.ib( + validator=attr.validators.not_(attr.validators.in_("abc"), msg="spam") + ) + p: Any = attr.ib( + validator=attr.validators.not_(attr.validators.in_("abc"), msg=None) + ) + q: Any = attr.ib( + validator=attrs.validators.optional(attrs.validators.instance_of(C)) + ) + r: Any = attr.ib( + validator=attrs.validators.optional([attrs.validators.instance_of(C)]) + ) + s: Any = attr.ib( + validator=attrs.validators.optional((attrs.validators.instance_of(C),)) + ) + @attr.define class Validated2: @@ -284,14 +324,14 @@ class ValidatedSetter2: # field_transformer -def ft_hook(cls: type, attribs: List[attr.Attribute]) -> List[attr.Attribute]: +def ft_hook(cls: type, attribs: list[attr.Attribute]) -> list[attr.Attribute]: return attribs # field_transformer def ft_hook2( - cls: type, attribs: List[attrs.Attribute] -) -> List[attrs.Attribute]: + cls: type, attribs: list[attrs.Attribute] +) -> list[attrs.Attribute]: return attribs @@ -355,16 +395,16 @@ class MRO: @attr.s class FactoryTest: - a: List[int] = attr.ib(default=attr.Factory(list)) - b: List[Any] = attr.ib(default=attr.Factory(list, False)) - c: List[int] = attr.ib(default=attr.Factory((lambda s: s.a), True)) + a: list[int] = attr.ib(default=attr.Factory(list)) + b: list[Any] = attr.ib(default=attr.Factory(list, False)) + c: list[int] = attr.ib(default=attr.Factory((lambda s: s.a), True)) @attrs.define class FactoryTest2: - a: List[int] = attrs.field(default=attrs.Factory(list)) - b: List[Any] = attrs.field(default=attrs.Factory(list, False)) - c: List[int] = attrs.field(default=attrs.Factory((lambda s: s.a), True)) + a: list[int] = attrs.field(default=attrs.Factory(list)) + b: list[Any] = attrs.field(default=attrs.Factory(list, False)) + c: list[int] = attrs.field(default=attrs.Factory((lambda s: s.a), True)) attrs.asdict(FactoryTest2()) @@ -394,27 +434,42 @@ attrs.asdict(MatchArgs2(1, 2)) attrs.astuple(MatchArgs2(1, 2)) -def importing_from_attr() -> None: +def accessing_from_attr() -> None: """ Use a function to keep the ns clean. """ - from attr.converters import optional - from attr.exceptions import FrozenError - from attr.filters import include - from attr.setters import frozen - from attr.validators import and_ - - assert optional and FrozenError and include and frozen and and_ + attr.converters.optional + attr.exceptions.FrozenError + attr.filters.include + attr.filters.exclude + attr.setters.frozen + attr.validators.and_ + attr.cmp_using -def importing_from_attrs() -> None: +def accessing_from_attrs() -> None: """ Use a function to keep the ns clean. """ - from attrs.converters import optional - from attrs.exceptions import FrozenError - from attrs.filters import include - from attrs.setters import frozen - from attrs.validators import and_ + attrs.converters.optional + attrs.exceptions.FrozenError + attrs.filters.include + attrs.filters.exclude + attrs.setters.frozen + attrs.validators.and_ + attrs.cmp_using + + +foo = object +if attrs.has(foo) or attr.has(foo): + foo.__attrs_attrs__ + + +@attrs.define(unsafe_hash=True) +class Hashable: + pass + - assert optional and FrozenError and include and frozen and and_ +def test(cls: type) -> None: + if attr.has(cls): + attr.resolve_types(cls) diff --git a/tests/wpt/tests/tools/third_party/attrs/tests/utils.py b/tests/wpt/tests/tools/third_party/attrs/tests/utils.py index a2fefbd6068..9e678f05f17 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tests/utils.py +++ b/tests/wpt/tests/tools/third_party/attrs/tests/utils.py @@ -4,10 +4,9 @@ Common helper functions for tests. """ -from __future__ import absolute_import, division, print_function from attr import Attribute -from attr._make import NOTHING, make_class +from attr._make import NOTHING, _default_init_alias_for, make_class def simple_class( @@ -65,22 +64,5 @@ def simple_attr( converter=converter, kw_only=kw_only, inherited=inherited, + alias=_default_init_alias_for(name), ) - - -class TestSimpleClass(object): - """ - Tests for the testing helper function `make_class`. - """ - - def test_returns_class(self): - """ - Returns a class object. - """ - assert type is simple_class().__class__ - - def returns_distinct_classes(self): - """ - Each call returns a completely new class. - """ - assert simple_class() is not simple_class() diff --git a/tests/wpt/tests/tools/third_party/attrs/tox.ini b/tests/wpt/tests/tools/third_party/attrs/tox.ini index ddcbc4dbbcd..54724faaafb 100644 --- a/tests/wpt/tests/tools/third_party/attrs/tox.ini +++ b/tests/wpt/tests/tools/third_party/attrs/tox.ini @@ -1,129 +1,114 @@ -[pytest] -addopts = -ra -testpaths = tests -xfail_strict = true -filterwarnings = - once::Warning - ignore:::pympler[.*] - - -# Keep docs in sync with docs env and .readthedocs.yml. -[gh-actions] -python = - 2.7: py27 - 3.5: py35 - 3.6: py36 - 3.7: py37 - 3.8: py38, changelog - 3.9: py39, pyright - 3.10: py310, manifest, typing, docs - pypy-2: pypy - pypy-3: pypy3 - - [tox] -envlist = typing,pre-commit,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report -isolated_build = True +min_version = 4 +env_list = + pre-commit, + py3{7,8,9,10,11,12}-tests, + py3{8,9,10,11,12}-mypy, + pypy3, + pyright, + docs, + changelog, + coverage-report -[testenv:docs] -# Keep basepython in sync with gh-actions and .readthedocs.yml. -basepython = python3.10 -extras = docs -commands = - sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html - python -m doctest README.rst +[testenv:.pkg] +pass_env = SETUPTOOLS_SCM_PRETEND_VERSION [testenv] -extras = tests -commands = python -m pytest {posargs} - - -[testenv:py27] -extras = tests -commands = coverage run -m pytest {posargs} - - -[testenv:py37] -extras = tests -commands = coverage run -m pytest {posargs} - +package = wheel +wheel_build_env = .pkg +pass_env = + FORCE_COLOR + NO_COLOR +extras = + tests: tests + mypy: tests-mypy +commands = + tests: pytest {posargs:-n auto} + mypy: mypy tests/typing_example.py + mypy: mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi -[testenv:py310] +[testenv:py3{7,10,12}-tests] +extras = cov # Python 3.6+ has a number of compile-time warnings on invalid string escapes. -# PYTHONWARNINGS=d and --no-compile below make them visible during the Tox run. -basepython = python3.10 -install_command = pip install --no-compile {opts} {packages} -setenv = +# PYTHONWARNINGS=d and --no-compile below make them visible during the tox run. +set_env = + COVERAGE_PROCESS_START={toxinidir}/pyproject.toml PYTHONWARNINGS=d -extras = tests -commands = coverage run -m pytest {posargs} +install_command = python -Im pip install --no-compile {opts} {packages} +commands_pre = python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")' +commands = coverage run -m pytest {posargs:-n auto} [testenv:coverage-report] -basepython = python3.10 -depends = py27,py37,py310 +# Keep base_python in-sync with .python-version-default +base_python = py312 +depends = py3{7,10,11} skip_install = true -deps = coverage[toml]>=5.4 +deps = coverage[toml]>=5.3 commands = coverage combine coverage report -[testenv:pre-commit] -basepython = python3.10 -skip_install = true -deps = - pre-commit -passenv = HOMEPATH # needed on Windows +[testenv:docs] +# Keep base_python in-sync with ci.yml/docs and .readthedocs.yaml. +base_python = py312 +extras = docs commands = - pre-commit run --all-files + sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html -[testenv:manifest] -basepython = python3.10 -deps = check-manifest -skip_install = true -commands = check-manifest +[testenv:docs-watch] +package = editable +base_python = {[testenv:docs]base_python} +extras = {[testenv:docs]extras} +deps = watchfiles +commands = + watchfiles \ + --ignore-paths docs/_build/ \ + 'sphinx-build -W -n --jobs auto -b html -d {envtmpdir}/doctrees docs docs/_build/html' \ + src \ + docs + + +[testenv:docs-linkcheck] +package = editable +base_python = {[testenv:docs]base_python} +extras = {[testenv:docs]extras} +commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html -[testenv:pypi-description] -basepython = python3.8 +[testenv:pre-commit] skip_install = true -deps = - twine - pip >= 18.0.0 -commands = - pip wheel -w {envtmpdir}/build --no-deps . - twine check {envtmpdir}/build/* +deps = pre-commit +commands = pre-commit run --all-files [testenv:changelog] -basepython = python3.8 -deps = towncrier<21.3 +deps = towncrier skip_install = true -commands = towncrier --draft +commands = towncrier build --version main --draft -[testenv:typing] -basepython = python3.10 -deps = mypy>=0.902 -commands = - mypy src/attr/__init__.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi - mypy tests/typing_example.py +[testenv:pyright] +extras = tests +deps = pyright +commands = pytest tests/test_pyright.py -vv -[testenv:pyright] -# Install and configure node and pyright -# This *could* be folded into a custom install_command -# Use nodeenv to configure node in the running tox virtual environment -# Seeing errors using "nodeenv -p" -# Use npm install -g to install "globally" into the virtual environment -basepython = python3.9 -deps = nodeenv +[testenv:docset] +deps = doc2dash +extras = docs +allowlist_externals = + rm + cp + tar commands = - nodeenv --prebuilt --node=lts --force {envdir} - npm install -g --no-package-lock --no-save pyright - pytest tests/test_pyright.py -vv + rm -rf attrs.docset attrs.tgz docs/_build + sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + doc2dash --index-page index.html --icon docs/_static/docset-icon.png --online-redirect-url https://www.attrs.org/en/latest/ docs/_build/html + cp docs/_static/docset-icon@2x.png attrs.docset/icon@2x.png + tar --exclude='.DS_Store' -cvzf attrs.tgz attrs.docset diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/CHANGES.rst b/tests/wpt/tests/tools/third_party/exceptiongroup/CHANGES.rst new file mode 100644 index 00000000000..ea8cbea8f7b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/CHANGES.rst @@ -0,0 +1,131 @@ +Version history +=============== + +This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_. + +**1.2.1** + +- Updated the copying of ``__notes__`` to match CPython behavior (PR by CF Bolz-Tereick) +- Corrected the type annotation of the exception handler callback to accept a + ``BaseExceptionGroup`` instead of ``BaseException`` +- Fixed type errors on Python < 3.10 and the type annotation of ``suppress()`` + (PR by John Litborn) + +**1.2.0** + +- Added special monkeypatching if `Apport <https://github.com/canonical/apport>`_ has + overridden ``sys.excepthook`` so it will format exception groups correctly + (PR by John Litborn) +- Added a backport of ``contextlib.suppress()`` from Python 3.12.1 which also handles + suppressing exceptions inside exception groups +- Fixed bare ``raise`` in a handler reraising the original naked exception rather than + an exception group which is what is raised when you do a ``raise`` in an ``except*`` + handler + +**1.1.3** + +- ``catch()`` now raises a ``TypeError`` if passed an async exception handler instead of + just giving a ``RuntimeWarning`` about the coroutine never being awaited. (#66, PR by + John Litborn) +- Fixed plain ``raise`` statement in an exception handler callback to work like a + ``raise`` in an ``except*`` block +- Fixed new exception group not being chained to the original exception when raising an + exception group from exceptions raised in handler callbacks +- Fixed type annotations of the ``derive()``, ``subgroup()`` and ``split()`` methods to + match the ones in typeshed + +**1.1.2** + +- Changed handling of exceptions in exception group handler callbacks to not wrap a + single exception in an exception group, as per + `CPython issue 103590 <https://github.com/python/cpython/issues/103590>`_ + +**1.1.1** + +- Worked around + `CPython issue #98778 <https://github.com/python/cpython/issues/98778>`_, + ``urllib.error.HTTPError(..., fp=None)`` raises ``KeyError`` on unknown attribute + access, on affected Python versions. (PR by Zac Hatfield-Dodds) + +**1.1.0** + +- Backported upstream fix for gh-99553 (custom subclasses of ``BaseExceptionGroup`` that + also inherit from ``Exception`` should not be able to wrap base exceptions) +- Moved all initialization code to ``__new__()`` (thus matching Python 3.11 behavior) + +**1.0.4** + +- Fixed regression introduced in v1.0.3 where the code computing the suggestions would + assume that both the ``obj`` attribute of ``AttributeError`` is always available, even + though this is only true from Python 3.10 onwards + (#43; PR by Carl Friedrich Bolz-Tereick) + +**1.0.3** + +- Fixed monkey patching breaking suggestions (on a ``NameError`` or ``AttributeError``) + on Python 3.10 (#41; PR by Carl Friedrich Bolz-Tereick) + +**1.0.2** + +- Updated type annotations to match the ones in ``typeshed`` + +**1.0.1** + +- Fixed formatted traceback missing exceptions beyond 2 nesting levels of + ``__context__`` or ``__cause__`` + +**1.0.0** + +- Fixed + ``AttributeError: 'PatchedTracebackException' object has no attribute '__cause__'`` + on Python 3.10 (only) when a traceback is printed from an exception where an exception + group is set as the cause (#33) +- Fixed a loop in exception groups being rendered incorrectly (#35) +- Fixed the patched formatting functions (``format_exception()``etc.) not passing the + ``compact=True`` flag on Python 3.10 like the original functions do + +**1.0.0rc9** + +- Added custom versions of several ``traceback`` functions that work with exception + groups even if monkey patching was disabled or blocked + +**1.0.0rc8** + +- Don't monkey patch anything if ``sys.excepthook`` has been altered +- Fixed formatting of ``SyntaxError`` in the monkey patched + ``TracebackException.format_exception_only()`` method + +**1.0.0rc7** + +- **BACKWARDS INCOMPATIBLE** Changed ``catch()`` to not wrap an exception in an + exception group if only one exception arrived at ``catch()`` and it was not matched + with any handlers. This was to match the behavior of ``except*``. + +**1.0.0rc6** + +- **BACKWARDS INCOMPATIBLE** Changed ``catch()`` to match the behavior of ``except*``: + each handler will be called only once per key in the ``handlers`` dictionary, and with + an exception group as the argument. Handlers now also catch subclasses of the given + exception types, just like ``except*``. + +**1.0.0rc5** + +- Patch for ``traceback.TracebackException.format_exception_only()`` (PR by Zac Hatfield-Dodds) + +**1.0.0rc4** + +- Update `PEP 678`_ support to use ``.add_note()`` and ``__notes__`` (PR by Zac Hatfield-Dodds) + +**1.0.0rc3** + +- Added message about the number of sub-exceptions + +**1.0.0rc2** + +- Display and copy ``__note__`` (draft `PEP 678`_) if available (PR by Zac Hatfield-Dodds) + +.. _PEP 678: https://www.python.org/dev/peps/pep-0678/ + +**1.0.0rc1** + +- Initial release diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/LICENSE b/tests/wpt/tests/tools/third_party/exceptiongroup/LICENSE new file mode 100644 index 00000000000..50d4fa5e684 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/LICENSE @@ -0,0 +1,73 @@ +The MIT License (MIT) + +Copyright (c) 2022 Alex Grönholm + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +This project contains code copied from the Python standard library. +The following is the required license notice for those parts. + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/PKG-INFO b/tests/wpt/tests/tools/third_party/exceptiongroup/PKG-INFO new file mode 100644 index 00000000000..2e8819b83b6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/PKG-INFO @@ -0,0 +1,157 @@ +Metadata-Version: 2.1 +Name: exceptiongroup +Version: 1.2.1 +Summary: Backport of PEP 654 (exception groups) +Author-email: Alex Grönholm <alex.gronholm@nextday.fi> +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Typing :: Typed +Requires-Dist: pytest >= 6 ; extra == "test" +Project-URL: Changelog, https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst +Project-URL: Issue Tracker, https://github.com/agronholm/exceptiongroup/issues +Project-URL: Source code, https://github.com/agronholm/exceptiongroup +Provides-Extra: test + +.. image:: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/exceptiongroup/badge.svg?branch=main + :target: https://coveralls.io/github/agronholm/exceptiongroup?branch=main + :alt: Code Coverage + +This is a backport of the ``BaseExceptionGroup`` and ``ExceptionGroup`` classes from +Python 3.11. + +It contains the following: + +* The ``exceptiongroup.BaseExceptionGroup`` and ``exceptiongroup.ExceptionGroup`` + classes +* A utility function (``exceptiongroup.catch()``) for catching exceptions possibly + nested in an exception group +* Patches to the ``TracebackException`` class that properly formats exception groups + (installed on import) +* An exception hook that handles formatting of exception groups through + ``TracebackException`` (installed on import) +* Special versions of some of the functions from the ``traceback`` module, modified to + correctly handle exception groups even when monkey patching is disabled, or blocked by + another custom exception hook: + + * ``traceback.format_exception()`` + * ``traceback.format_exception_only()`` + * ``traceback.print_exception()`` + * ``traceback.print_exc()`` +* A backported version of ``contextlib.suppress()`` from Python 3.12.1 which also + handles suppressing exceptions inside exception groups + +If this package is imported on Python 3.11 or later, the built-in implementations of the +exception group classes are used instead, ``TracebackException`` is not monkey patched +and the exception hook won't be installed. + +See the `standard library documentation`_ for more information on exception groups. + +.. _standard library documentation: https://docs.python.org/3/library/exceptions.html + +Catching exceptions +=================== + +Due to the lack of the ``except*`` syntax introduced by `PEP 654`_ in earlier Python +versions, you need to use ``exceptiongroup.catch()`` to catch exceptions that are +potentially nested inside an exception group. This function returns a context manager +that calls the given handler for any exceptions matching the sole argument. + +The argument to ``catch()`` must be a dict (or any ``Mapping``) where each key is either +an exception class or an iterable of exception classes. Each value must be a callable +that takes a single positional argument. The handler will be called at most once, with +an exception group as an argument which will contain all the exceptions that are any +of the given types, or their subclasses. The exception group may contain nested groups +containing more matching exceptions. + +Thus, the following Python 3.11+ code: + +.. code-block:: python + + try: + ... + except* (ValueError, KeyError) as excgroup: + for exc in excgroup.exceptions: + print('Caught exception:', type(exc)) + except* RuntimeError: + print('Caught runtime error') + +would be written with this backport like this: + +.. code-block:: python + + from exceptiongroup import BaseExceptionGroup, catch + + def value_key_err_handler(excgroup: BaseExceptionGroup) -> None: + for exc in excgroup.exceptions: + print('Caught exception:', type(exc)) + + def runtime_err_handler(exc: BaseExceptionGroup) -> None: + print('Caught runtime error') + + with catch({ + (ValueError, KeyError): value_key_err_handler, + RuntimeError: runtime_err_handler + }): + ... + +**NOTE**: Just like with ``except*``, you cannot handle ``BaseExceptionGroup`` or +``ExceptionGroup`` with ``catch()``. + +Suppressing exceptions +====================== + +This library contains a backport of the ``contextlib.suppress()`` context manager from +Python 3.12.1. It allows you to selectively ignore certain exceptions, even when they're +inside exception groups: + +.. code-block:: python + + from exceptiongroup import suppress + + with suppress(RuntimeError): + raise ExceptionGroup("", [RuntimeError("boo")]) + +Notes on monkey patching +======================== + +To make exception groups render properly when an unhandled exception group is being +printed out, this package does two things when it is imported on any Python version +earlier than 3.11: + +#. The ``traceback.TracebackException`` class is monkey patched to store extra + information about exception groups (in ``__init__()``) and properly format them (in + ``format()``) +#. An exception hook is installed at ``sys.excepthook``, provided that no other hook is + already present. This hook causes the exception to be formatted using + ``traceback.TracebackException`` rather than the built-in rendered. + +If ``sys.exceptionhook`` is found to be set to something else than the default when +``exceptiongroup`` is imported, no monkeypatching is done at all. + +To prevent the exception hook and patches from being installed, set the environment +variable ``EXCEPTIONGROUP_NO_PATCH`` to ``1``. + +Formatting exception groups +--------------------------- + +Normally, the monkey patching applied by this library on import will cause exception +groups to be printed properly in tracebacks. But in cases when the monkey patching is +blocked by a third party exception hook, or monkey patching is explicitly disabled, +you can still manually format exceptions using the special versions of the ``traceback`` +functions, like ``format_exception()``, listed at the top of this page. They work just +like their counterparts in the ``traceback`` module, except that they use a separately +patched subclass of ``TracebackException`` to perform the rendering. + +Particularly in cases where a library installs its own exception hook, it is recommended +to use these special versions to do the actual formatting of exceptions/tracebacks. + +.. _PEP 654: https://www.python.org/dev/peps/pep-0654/ + diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/README.rst b/tests/wpt/tests/tools/third_party/exceptiongroup/README.rst new file mode 100644 index 00000000000..d34937d576e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/README.rst @@ -0,0 +1,137 @@ +.. image:: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/exceptiongroup/badge.svg?branch=main + :target: https://coveralls.io/github/agronholm/exceptiongroup?branch=main + :alt: Code Coverage + +This is a backport of the ``BaseExceptionGroup`` and ``ExceptionGroup`` classes from +Python 3.11. + +It contains the following: + +* The ``exceptiongroup.BaseExceptionGroup`` and ``exceptiongroup.ExceptionGroup`` + classes +* A utility function (``exceptiongroup.catch()``) for catching exceptions possibly + nested in an exception group +* Patches to the ``TracebackException`` class that properly formats exception groups + (installed on import) +* An exception hook that handles formatting of exception groups through + ``TracebackException`` (installed on import) +* Special versions of some of the functions from the ``traceback`` module, modified to + correctly handle exception groups even when monkey patching is disabled, or blocked by + another custom exception hook: + + * ``traceback.format_exception()`` + * ``traceback.format_exception_only()`` + * ``traceback.print_exception()`` + * ``traceback.print_exc()`` +* A backported version of ``contextlib.suppress()`` from Python 3.12.1 which also + handles suppressing exceptions inside exception groups + +If this package is imported on Python 3.11 or later, the built-in implementations of the +exception group classes are used instead, ``TracebackException`` is not monkey patched +and the exception hook won't be installed. + +See the `standard library documentation`_ for more information on exception groups. + +.. _standard library documentation: https://docs.python.org/3/library/exceptions.html + +Catching exceptions +=================== + +Due to the lack of the ``except*`` syntax introduced by `PEP 654`_ in earlier Python +versions, you need to use ``exceptiongroup.catch()`` to catch exceptions that are +potentially nested inside an exception group. This function returns a context manager +that calls the given handler for any exceptions matching the sole argument. + +The argument to ``catch()`` must be a dict (or any ``Mapping``) where each key is either +an exception class or an iterable of exception classes. Each value must be a callable +that takes a single positional argument. The handler will be called at most once, with +an exception group as an argument which will contain all the exceptions that are any +of the given types, or their subclasses. The exception group may contain nested groups +containing more matching exceptions. + +Thus, the following Python 3.11+ code: + +.. code-block:: python + + try: + ... + except* (ValueError, KeyError) as excgroup: + for exc in excgroup.exceptions: + print('Caught exception:', type(exc)) + except* RuntimeError: + print('Caught runtime error') + +would be written with this backport like this: + +.. code-block:: python + + from exceptiongroup import BaseExceptionGroup, catch + + def value_key_err_handler(excgroup: BaseExceptionGroup) -> None: + for exc in excgroup.exceptions: + print('Caught exception:', type(exc)) + + def runtime_err_handler(exc: BaseExceptionGroup) -> None: + print('Caught runtime error') + + with catch({ + (ValueError, KeyError): value_key_err_handler, + RuntimeError: runtime_err_handler + }): + ... + +**NOTE**: Just like with ``except*``, you cannot handle ``BaseExceptionGroup`` or +``ExceptionGroup`` with ``catch()``. + +Suppressing exceptions +====================== + +This library contains a backport of the ``contextlib.suppress()`` context manager from +Python 3.12.1. It allows you to selectively ignore certain exceptions, even when they're +inside exception groups: + +.. code-block:: python + + from exceptiongroup import suppress + + with suppress(RuntimeError): + raise ExceptionGroup("", [RuntimeError("boo")]) + +Notes on monkey patching +======================== + +To make exception groups render properly when an unhandled exception group is being +printed out, this package does two things when it is imported on any Python version +earlier than 3.11: + +#. The ``traceback.TracebackException`` class is monkey patched to store extra + information about exception groups (in ``__init__()``) and properly format them (in + ``format()``) +#. An exception hook is installed at ``sys.excepthook``, provided that no other hook is + already present. This hook causes the exception to be formatted using + ``traceback.TracebackException`` rather than the built-in rendered. + +If ``sys.exceptionhook`` is found to be set to something else than the default when +``exceptiongroup`` is imported, no monkeypatching is done at all. + +To prevent the exception hook and patches from being installed, set the environment +variable ``EXCEPTIONGROUP_NO_PATCH`` to ``1``. + +Formatting exception groups +--------------------------- + +Normally, the monkey patching applied by this library on import will cause exception +groups to be printed properly in tracebacks. But in cases when the monkey patching is +blocked by a third party exception hook, or monkey patching is explicitly disabled, +you can still manually format exceptions using the special versions of the ``traceback`` +functions, like ``format_exception()``, listed at the top of this page. They work just +like their counterparts in the ``traceback`` module, except that they use a separately +patched subclass of ``TracebackException`` to perform the rendering. + +Particularly in cases where a library installs its own exception hook, it is recommended +to use these special versions to do the actual formatting of exceptions/tracebacks. + +.. _PEP 654: https://www.python.org/dev/peps/pep-0654/ diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/pyproject.toml b/tests/wpt/tests/tools/third_party/exceptiongroup/pyproject.toml new file mode 100644 index 00000000000..aa47cdcec4c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/pyproject.toml @@ -0,0 +1,111 @@ +[build-system] +requires = ["flit_scm"] +build-backend = "flit_scm:buildapi" + +[project] +name = "exceptiongroup" +description = "Backport of PEP 654 (exception groups)" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Typing :: Typed" +] +authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}] +license = {file = "LICENSE"} +requires-python = ">=3.7" +dynamic = ["version"] + +[project.urls] +Changelog = "https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst" +"Source code" = "https://github.com/agronholm/exceptiongroup" +"Issue Tracker" = "https://github.com/agronholm/exceptiongroup/issues" + +[project.optional-dependencies] +test = [ + "pytest >= 6" +] + +[tool.flit.sdist] +include = [ + "CHANGES.rst", + "tests", +] +exclude = [ + ".github/*", + ".gitignore", + ".pre-commit-config.yaml" +] + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "dirty-tag" +write_to = "src/exceptiongroup/_version.py" + +[tool.ruff.lint] +select = [ + "E", "F", "W", # default flake-8 + "I", # isort + "ISC", # flake8-implicit-str-concat + "PGH", # pygrep-hooks + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade +] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + +[tool.ruff.lint.isort] +known-first-party = ["exceptiongroup"] + +[tool.pytest.ini_options] +addopts = "-rsx --tb=short --strict-config --strict-markers" +testpaths = ["tests"] + +[tool.coverage.run] +source = ["exceptiongroup"] +relative_files = true + +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", + "@overload", +] + +[tool.pyright] +# for type tests, the code itself isn't type checked in CI +reportUnnecessaryTypeIgnoreComment = true + +[tool.mypy] +# for type tests, the code itself isn't type checked in CI +warn_unused_ignores = true + +[tool.tox] +legacy_tox_ini = """ +[tox] +envlist = py37, py38, py39, py310, py311, py312, pypy3 +labels = + typing = py{310,311,312}-typing +skip_missing_interpreters = true +minversion = 4.0 + +[testenv] +extras = test +commands = python -m pytest {posargs} +package = editable +usedevelop = true + +[testenv:{py37-,py38-,py39-,py310-,py311-,py312-,}typing] +deps = + pyright + mypy +commands = + pyright --verifytypes exceptiongroup + pyright tests/check_types.py + mypy tests/check_types.py +usedevelop = true +""" diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/__init__.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/__init__.py new file mode 100644 index 00000000000..d8e36b2e65d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/__init__.py @@ -0,0 +1,46 @@ +__all__ = [ + "BaseExceptionGroup", + "ExceptionGroup", + "catch", + "format_exception", + "format_exception_only", + "print_exception", + "print_exc", + "suppress", +] + +import os +import sys + +from ._catch import catch +from ._version import version as __version__ # noqa: F401 + +if sys.version_info < (3, 11): + from ._exceptions import BaseExceptionGroup, ExceptionGroup + from ._formatting import ( + format_exception, + format_exception_only, + print_exc, + print_exception, + ) + + if os.getenv("EXCEPTIONGROUP_NO_PATCH") != "1": + from . import _formatting # noqa: F401 + + BaseExceptionGroup.__module__ = __name__ + ExceptionGroup.__module__ = __name__ +else: + from traceback import ( + format_exception, + format_exception_only, + print_exc, + print_exception, + ) + + BaseExceptionGroup = BaseExceptionGroup + ExceptionGroup = ExceptionGroup + +if sys.version_info < (3, 12, 1): + from ._suppress import suppress +else: + from contextlib import suppress diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_catch.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_catch.py new file mode 100644 index 00000000000..0246568bd05 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_catch.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import inspect +import sys +from collections.abc import Callable, Iterable, Mapping +from contextlib import AbstractContextManager +from types import TracebackType +from typing import TYPE_CHECKING, Any + +if sys.version_info < (3, 11): + from ._exceptions import BaseExceptionGroup + +if TYPE_CHECKING: + _Handler = Callable[[BaseExceptionGroup[Any]], Any] + + +class _Catcher: + def __init__(self, handler_map: Mapping[tuple[type[BaseException], ...], _Handler]): + self._handler_map = handler_map + + def __enter__(self) -> None: + pass + + def __exit__( + self, + etype: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> bool: + if exc is not None: + unhandled = self.handle_exception(exc) + if unhandled is exc: + return False + elif unhandled is None: + return True + else: + if isinstance(exc, BaseExceptionGroup): + try: + raise unhandled from exc.__cause__ + except BaseExceptionGroup: + # Change __context__ to __cause__ because Python 3.11 does this + # too + unhandled.__context__ = exc.__cause__ + raise + + raise unhandled from exc + + return False + + def handle_exception(self, exc: BaseException) -> BaseException | None: + excgroup: BaseExceptionGroup | None + if isinstance(exc, BaseExceptionGroup): + excgroup = exc + else: + excgroup = BaseExceptionGroup("", [exc]) + + new_exceptions: list[BaseException] = [] + for exc_types, handler in self._handler_map.items(): + matched, excgroup = excgroup.split(exc_types) + if matched: + try: + try: + raise matched + except BaseExceptionGroup: + result = handler(matched) + except BaseExceptionGroup as new_exc: + if new_exc is matched: + new_exceptions.append(new_exc) + else: + new_exceptions.extend(new_exc.exceptions) + except BaseException as new_exc: + new_exceptions.append(new_exc) + else: + if inspect.iscoroutine(result): + raise TypeError( + f"Error trying to handle {matched!r} with {handler!r}. " + "Exception handler must be a sync function." + ) from exc + + if not excgroup: + break + + if new_exceptions: + if len(new_exceptions) == 1: + return new_exceptions[0] + + return BaseExceptionGroup("", new_exceptions) + elif ( + excgroup and len(excgroup.exceptions) == 1 and excgroup.exceptions[0] is exc + ): + return exc + else: + return excgroup + + +def catch( + __handlers: Mapping[type[BaseException] | Iterable[type[BaseException]], _Handler], +) -> AbstractContextManager[None]: + if not isinstance(__handlers, Mapping): + raise TypeError("the argument must be a mapping") + + handler_map: dict[ + tuple[type[BaseException], ...], Callable[[BaseExceptionGroup]] + ] = {} + for type_or_iterable, handler in __handlers.items(): + iterable: tuple[type[BaseException]] + if isinstance(type_or_iterable, type) and issubclass( + type_or_iterable, BaseException + ): + iterable = (type_or_iterable,) + elif isinstance(type_or_iterable, Iterable): + iterable = tuple(type_or_iterable) + else: + raise TypeError( + "each key must be either an exception classes or an iterable thereof" + ) + + if not callable(handler): + raise TypeError("handlers must be callable") + + for exc_type in iterable: + if not isinstance(exc_type, type) or not issubclass( + exc_type, BaseException + ): + raise TypeError( + "each key must be either an exception classes or an iterable " + "thereof" + ) + + if issubclass(exc_type, BaseExceptionGroup): + raise TypeError( + "catching ExceptionGroup with catch() is not allowed. " + "Use except instead." + ) + + handler_map[iterable] = handler + + return _Catcher(handler_map) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_exceptions.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_exceptions.py new file mode 100644 index 00000000000..a4a7acea822 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_exceptions.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +from collections.abc import Callable, Sequence +from functools import partial +from inspect import getmro, isclass +from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload + +_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True) +_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException) +_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True) +_ExceptionT = TypeVar("_ExceptionT", bound=Exception) +# using typing.Self would require a typing_extensions dependency on py<3.11 +_ExceptionGroupSelf = TypeVar("_ExceptionGroupSelf", bound="ExceptionGroup") +_BaseExceptionGroupSelf = TypeVar("_BaseExceptionGroupSelf", bound="BaseExceptionGroup") + + +def check_direct_subclass( + exc: BaseException, parents: tuple[type[BaseException]] +) -> bool: + for cls in getmro(exc.__class__)[:-1]: + if cls in parents: + return True + + return False + + +def get_condition_filter( + condition: type[_BaseExceptionT] + | tuple[type[_BaseExceptionT], ...] + | Callable[[_BaseExceptionT_co], bool], +) -> Callable[[_BaseExceptionT_co], bool]: + if isclass(condition) and issubclass( + cast(Type[BaseException], condition), BaseException + ): + return partial(check_direct_subclass, parents=(condition,)) + elif isinstance(condition, tuple): + if all(isclass(x) and issubclass(x, BaseException) for x in condition): + return partial(check_direct_subclass, parents=condition) + elif callable(condition): + return cast("Callable[[BaseException], bool]", condition) + + raise TypeError("expected a function, exception type or tuple of exception types") + + +def _derive_and_copy_attributes(self, excs): + eg = self.derive(excs) + eg.__cause__ = self.__cause__ + eg.__context__ = self.__context__ + eg.__traceback__ = self.__traceback__ + if hasattr(self, "__notes__"): + # Create a new list so that add_note() only affects one exceptiongroup + eg.__notes__ = list(self.__notes__) + return eg + + +class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]): + """A combination of multiple unrelated exceptions.""" + + def __new__( + cls: type[_BaseExceptionGroupSelf], + __message: str, + __exceptions: Sequence[_BaseExceptionT_co], + ) -> _BaseExceptionGroupSelf: + if not isinstance(__message, str): + raise TypeError(f"argument 1 must be str, not {type(__message)}") + if not isinstance(__exceptions, Sequence): + raise TypeError("second argument (exceptions) must be a sequence") + if not __exceptions: + raise ValueError( + "second argument (exceptions) must be a non-empty sequence" + ) + + for i, exc in enumerate(__exceptions): + if not isinstance(exc, BaseException): + raise ValueError( + f"Item {i} of second argument (exceptions) is not an exception" + ) + + if cls is BaseExceptionGroup: + if all(isinstance(exc, Exception) for exc in __exceptions): + cls = ExceptionGroup + + if issubclass(cls, Exception): + for exc in __exceptions: + if not isinstance(exc, Exception): + if cls is ExceptionGroup: + raise TypeError( + "Cannot nest BaseExceptions in an ExceptionGroup" + ) + else: + raise TypeError( + f"Cannot nest BaseExceptions in {cls.__name__!r}" + ) + + instance = super().__new__(cls, __message, __exceptions) + instance._message = __message + instance._exceptions = __exceptions + return instance + + def add_note(self, note: str) -> None: + if not isinstance(note, str): + raise TypeError( + f"Expected a string, got note={note!r} (type {type(note).__name__})" + ) + + if not hasattr(self, "__notes__"): + self.__notes__: list[str] = [] + + self.__notes__.append(note) + + @property + def message(self) -> str: + return self._message + + @property + def exceptions( + self, + ) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: + return tuple(self._exceptions) + + @overload + def subgroup( + self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] + ) -> ExceptionGroup[_ExceptionT] | None: ... + + @overload + def subgroup( + self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] + ) -> BaseExceptionGroup[_BaseExceptionT] | None: ... + + @overload + def subgroup( + self, + __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], + ) -> BaseExceptionGroup[_BaseExceptionT_co] | None: ... + + def subgroup( + self, + __condition: type[_BaseExceptionT] + | tuple[type[_BaseExceptionT], ...] + | Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], + ) -> BaseExceptionGroup[_BaseExceptionT] | None: + condition = get_condition_filter(__condition) + modified = False + if condition(self): + return self + + exceptions: list[BaseException] = [] + for exc in self.exceptions: + if isinstance(exc, BaseExceptionGroup): + subgroup = exc.subgroup(__condition) + if subgroup is not None: + exceptions.append(subgroup) + + if subgroup is not exc: + modified = True + elif condition(exc): + exceptions.append(exc) + else: + modified = True + + if not modified: + return self + elif exceptions: + group = _derive_and_copy_attributes(self, exceptions) + return group + else: + return None + + @overload + def split( + self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] + ) -> tuple[ + ExceptionGroup[_ExceptionT] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ]: ... + + @overload + def split( + self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] + ) -> tuple[ + BaseExceptionGroup[_BaseExceptionT] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ]: ... + + @overload + def split( + self, + __condition: Callable[[_BaseExceptionT_co | _BaseExceptionGroupSelf], bool], + ) -> tuple[ + BaseExceptionGroup[_BaseExceptionT_co] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ]: ... + + def split( + self, + __condition: type[_BaseExceptionT] + | tuple[type[_BaseExceptionT], ...] + | Callable[[_BaseExceptionT_co], bool], + ) -> ( + tuple[ + ExceptionGroup[_ExceptionT] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ] + | tuple[ + BaseExceptionGroup[_BaseExceptionT] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ] + | tuple[ + BaseExceptionGroup[_BaseExceptionT_co] | None, + BaseExceptionGroup[_BaseExceptionT_co] | None, + ] + ): + condition = get_condition_filter(__condition) + if condition(self): + return self, None + + matching_exceptions: list[BaseException] = [] + nonmatching_exceptions: list[BaseException] = [] + for exc in self.exceptions: + if isinstance(exc, BaseExceptionGroup): + matching, nonmatching = exc.split(condition) + if matching is not None: + matching_exceptions.append(matching) + + if nonmatching is not None: + nonmatching_exceptions.append(nonmatching) + elif condition(exc): + matching_exceptions.append(exc) + else: + nonmatching_exceptions.append(exc) + + matching_group: _BaseExceptionGroupSelf | None = None + if matching_exceptions: + matching_group = _derive_and_copy_attributes(self, matching_exceptions) + + nonmatching_group: _BaseExceptionGroupSelf | None = None + if nonmatching_exceptions: + nonmatching_group = _derive_and_copy_attributes( + self, nonmatching_exceptions + ) + + return matching_group, nonmatching_group + + @overload + def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]: ... + + @overload + def derive( + self, __excs: Sequence[_BaseExceptionT] + ) -> BaseExceptionGroup[_BaseExceptionT]: ... + + def derive( + self, __excs: Sequence[_BaseExceptionT] + ) -> BaseExceptionGroup[_BaseExceptionT]: + return BaseExceptionGroup(self.message, __excs) + + def __str__(self) -> str: + suffix = "" if len(self._exceptions) == 1 else "s" + return f"{self.message} ({len(self._exceptions)} sub-exception{suffix})" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.message!r}, {self._exceptions!r})" + + +class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception): + def __new__( + cls: type[_ExceptionGroupSelf], + __message: str, + __exceptions: Sequence[_ExceptionT_co], + ) -> _ExceptionGroupSelf: + return super().__new__(cls, __message, __exceptions) + + if TYPE_CHECKING: + + @property + def exceptions( + self, + ) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]: ... + + @overload # type: ignore[override] + def subgroup( + self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] + ) -> ExceptionGroup[_ExceptionT] | None: ... + + @overload + def subgroup( + self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] + ) -> ExceptionGroup[_ExceptionT_co] | None: ... + + def subgroup( + self, + __condition: type[_ExceptionT] + | tuple[type[_ExceptionT], ...] + | Callable[[_ExceptionT_co], bool], + ) -> ExceptionGroup[_ExceptionT] | None: + return super().subgroup(__condition) + + @overload + def split( + self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] + ) -> tuple[ + ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None + ]: ... + + @overload + def split( + self, __condition: Callable[[_ExceptionT_co | _ExceptionGroupSelf], bool] + ) -> tuple[ + ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None + ]: ... + + def split( + self: _ExceptionGroupSelf, + __condition: type[_ExceptionT] + | tuple[type[_ExceptionT], ...] + | Callable[[_ExceptionT_co], bool], + ) -> tuple[ + ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None + ]: + return super().split(__condition) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_formatting.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_formatting.py new file mode 100644 index 00000000000..e3835e41450 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_formatting.py @@ -0,0 +1,603 @@ +# traceback_exception_init() adapted from trio +# +# _ExceptionPrintContext and traceback_exception_format() copied from the standard +# library +from __future__ import annotations + +import collections.abc +import sys +import textwrap +import traceback +from functools import singledispatch +from types import TracebackType +from typing import Any, List, Optional + +from ._exceptions import BaseExceptionGroup + +max_group_width = 15 +max_group_depth = 10 +_cause_message = ( + "\nThe above exception was the direct cause of the following exception:\n\n" +) + +_context_message = ( + "\nDuring handling of the above exception, another exception occurred:\n\n" +) + + +def _format_final_exc_line(etype, value): + valuestr = _safe_string(value, "exception") + if value is None or not valuestr: + line = f"{etype}\n" + else: + line = f"{etype}: {valuestr}\n" + + return line + + +def _safe_string(value, what, func=str): + try: + return func(value) + except BaseException: + return f"<{what} {func.__name__}() failed>" + + +class _ExceptionPrintContext: + def __init__(self): + self.seen = set() + self.exception_group_depth = 0 + self.need_close = False + + def indent(self): + return " " * (2 * self.exception_group_depth) + + def emit(self, text_gen, margin_char=None): + if margin_char is None: + margin_char = "|" + indent_str = self.indent() + if self.exception_group_depth: + indent_str += margin_char + " " + + if isinstance(text_gen, str): + yield textwrap.indent(text_gen, indent_str, lambda line: True) + else: + for text in text_gen: + yield textwrap.indent(text, indent_str, lambda line: True) + + +def exceptiongroup_excepthook( + etype: type[BaseException], value: BaseException, tb: TracebackType | None +) -> None: + sys.stderr.write("".join(traceback.format_exception(etype, value, tb))) + + +class PatchedTracebackException(traceback.TracebackException): + def __init__( + self, + exc_type: type[BaseException], + exc_value: BaseException, + exc_traceback: TracebackType | None, + *, + limit: int | None = None, + lookup_lines: bool = True, + capture_locals: bool = False, + compact: bool = False, + _seen: set[int] | None = None, + ) -> None: + kwargs: dict[str, Any] = {} + if sys.version_info >= (3, 10): + kwargs["compact"] = compact + + is_recursive_call = _seen is not None + if _seen is None: + _seen = set() + _seen.add(id(exc_value)) + + self.stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_traceback), + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + ) + self.exc_type = exc_type + # Capture now to permit freeing resources: only complication is in the + # unofficial API _format_final_exc_line + self._str = _safe_string(exc_value, "exception") + try: + self.__notes__ = getattr(exc_value, "__notes__", None) + except KeyError: + # Workaround for https://github.com/python/cpython/issues/98778 on Python + # <= 3.9, and some 3.10 and 3.11 patch versions. + HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) + if sys.version_info[:2] <= (3, 11) and isinstance(exc_value, HTTPError): + self.__notes__ = None + else: + raise + + if exc_type and issubclass(exc_type, SyntaxError): + # Handle SyntaxError's specially + self.filename = exc_value.filename + lno = exc_value.lineno + self.lineno = str(lno) if lno is not None else None + self.text = exc_value.text + self.offset = exc_value.offset + self.msg = exc_value.msg + if sys.version_info >= (3, 10): + end_lno = exc_value.end_lineno + self.end_lineno = str(end_lno) if end_lno is not None else None + self.end_offset = exc_value.end_offset + elif ( + exc_type + and issubclass(exc_type, (NameError, AttributeError)) + and getattr(exc_value, "name", None) is not None + ): + suggestion = _compute_suggestion_error(exc_value, exc_traceback) + if suggestion: + self._str += f". Did you mean: '{suggestion}'?" + + if lookup_lines: + # Force all lines in the stack to be loaded + for frame in self.stack: + frame.line + + self.__suppress_context__ = ( + exc_value.__suppress_context__ if exc_value is not None else False + ) + + # Convert __cause__ and __context__ to `TracebackExceptions`s, use a + # queue to avoid recursion (only the top-level call gets _seen == None) + if not is_recursive_call: + queue = [(self, exc_value)] + while queue: + te, e = queue.pop() + + if e and e.__cause__ is not None and id(e.__cause__) not in _seen: + cause = PatchedTracebackException( + type(e.__cause__), + e.__cause__, + e.__cause__.__traceback__, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=_seen, + ) + else: + cause = None + + if compact: + need_context = ( + cause is None and e is not None and not e.__suppress_context__ + ) + else: + need_context = True + if ( + e + and e.__context__ is not None + and need_context + and id(e.__context__) not in _seen + ): + context = PatchedTracebackException( + type(e.__context__), + e.__context__, + e.__context__.__traceback__, + limit=limit, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=_seen, + ) + else: + context = None + + # Capture each of the exceptions in the ExceptionGroup along with each + # of their causes and contexts + if e and isinstance(e, BaseExceptionGroup): + exceptions = [] + for exc in e.exceptions: + texc = PatchedTracebackException( + type(exc), + exc, + exc.__traceback__, + lookup_lines=lookup_lines, + capture_locals=capture_locals, + _seen=_seen, + ) + exceptions.append(texc) + else: + exceptions = None + + te.__cause__ = cause + te.__context__ = context + te.exceptions = exceptions + if cause: + queue.append((te.__cause__, e.__cause__)) + if context: + queue.append((te.__context__, e.__context__)) + if exceptions: + queue.extend(zip(te.exceptions, e.exceptions)) + + def format(self, *, chain=True, _ctx=None): + if _ctx is None: + _ctx = _ExceptionPrintContext() + + output = [] + exc = self + if chain: + while exc: + if exc.__cause__ is not None: + chained_msg = _cause_message + chained_exc = exc.__cause__ + elif exc.__context__ is not None and not exc.__suppress_context__: + chained_msg = _context_message + chained_exc = exc.__context__ + else: + chained_msg = None + chained_exc = None + + output.append((chained_msg, exc)) + exc = chained_exc + else: + output.append((None, exc)) + + for msg, exc in reversed(output): + if msg is not None: + yield from _ctx.emit(msg) + if exc.exceptions is None: + if exc.stack: + yield from _ctx.emit("Traceback (most recent call last):\n") + yield from _ctx.emit(exc.stack.format()) + yield from _ctx.emit(exc.format_exception_only()) + elif _ctx.exception_group_depth > max_group_depth: + # exception group, but depth exceeds limit + yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n") + else: + # format exception group + is_toplevel = _ctx.exception_group_depth == 0 + if is_toplevel: + _ctx.exception_group_depth += 1 + + if exc.stack: + yield from _ctx.emit( + "Exception Group Traceback (most recent call last):\n", + margin_char="+" if is_toplevel else None, + ) + yield from _ctx.emit(exc.stack.format()) + + yield from _ctx.emit(exc.format_exception_only()) + num_excs = len(exc.exceptions) + if num_excs <= max_group_width: + n = num_excs + else: + n = max_group_width + 1 + _ctx.need_close = False + for i in range(n): + last_exc = i == n - 1 + if last_exc: + # The closing frame may be added by a recursive call + _ctx.need_close = True + + if max_group_width is not None: + truncated = i >= max_group_width + else: + truncated = False + title = f"{i + 1}" if not truncated else "..." + yield ( + _ctx.indent() + + ("+-" if i == 0 else " ") + + f"+---------------- {title} ----------------\n" + ) + _ctx.exception_group_depth += 1 + if not truncated: + yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx) + else: + remaining = num_excs - max_group_width + plural = "s" if remaining > 1 else "" + yield from _ctx.emit( + f"and {remaining} more exception{plural}\n" + ) + + if last_exc and _ctx.need_close: + yield _ctx.indent() + "+------------------------------------\n" + _ctx.need_close = False + _ctx.exception_group_depth -= 1 + + if is_toplevel: + assert _ctx.exception_group_depth == 1 + _ctx.exception_group_depth = 0 + + def format_exception_only(self): + """Format the exception part of the traceback. + The return value is a generator of strings, each ending in a newline. + Normally, the generator emits a single string; however, for + SyntaxError exceptions, it emits several lines that (when + printed) display detailed information about where the syntax + error occurred. + The message indicating which exception occurred is always the last + string in the output. + """ + if self.exc_type is None: + yield traceback._format_final_exc_line(None, self._str) + return + + stype = self.exc_type.__qualname__ + smod = self.exc_type.__module__ + if smod not in ("__main__", "builtins"): + if not isinstance(smod, str): + smod = "<unknown>" + stype = smod + "." + stype + + if not issubclass(self.exc_type, SyntaxError): + yield _format_final_exc_line(stype, self._str) + elif traceback_exception_format_syntax_error is not None: + yield from traceback_exception_format_syntax_error(self, stype) + else: + yield from traceback_exception_original_format_exception_only(self) + + if isinstance(self.__notes__, collections.abc.Sequence): + for note in self.__notes__: + note = _safe_string(note, "note") + yield from [line + "\n" for line in note.split("\n")] + elif self.__notes__ is not None: + yield _safe_string(self.__notes__, "__notes__", func=repr) + + +traceback_exception_original_format = traceback.TracebackException.format +traceback_exception_original_format_exception_only = ( + traceback.TracebackException.format_exception_only +) +traceback_exception_format_syntax_error = getattr( + traceback.TracebackException, "_format_syntax_error", None +) +if sys.excepthook is sys.__excepthook__: + traceback.TracebackException.__init__ = ( # type: ignore[assignment] + PatchedTracebackException.__init__ + ) + traceback.TracebackException.format = ( # type: ignore[assignment] + PatchedTracebackException.format + ) + traceback.TracebackException.format_exception_only = ( # type: ignore[assignment] + PatchedTracebackException.format_exception_only + ) + sys.excepthook = exceptiongroup_excepthook + +# Ubuntu's system Python has a sitecustomize.py file that imports +# apport_python_hook and replaces sys.excepthook. +# +# The custom hook captures the error for crash reporting, and then calls +# sys.__excepthook__ to actually print the error. +# +# We don't mind it capturing the error for crash reporting, but we want to +# take over printing the error. So we monkeypatch the apport_python_hook +# module so that instead of calling sys.__excepthook__, it calls our custom +# hook. +# +# More details: https://github.com/python-trio/trio/issues/1065 +if getattr(sys.excepthook, "__name__", None) in ( + "apport_excepthook", + # on ubuntu 22.10 the hook was renamed to partial_apport_excepthook + "partial_apport_excepthook", +): + # patch traceback like above + traceback.TracebackException.__init__ = ( # type: ignore[assignment] + PatchedTracebackException.__init__ + ) + traceback.TracebackException.format = ( # type: ignore[assignment] + PatchedTracebackException.format + ) + traceback.TracebackException.format_exception_only = ( # type: ignore[assignment] + PatchedTracebackException.format_exception_only + ) + + from types import ModuleType + + import apport_python_hook + + assert sys.excepthook is apport_python_hook.apport_excepthook + + # monkeypatch the sys module that apport has imported + fake_sys = ModuleType("exceptiongroup_fake_sys") + fake_sys.__dict__.update(sys.__dict__) + fake_sys.__excepthook__ = exceptiongroup_excepthook + apport_python_hook.sys = fake_sys + + +@singledispatch +def format_exception_only(__exc: BaseException) -> List[str]: + return list( + PatchedTracebackException( + type(__exc), __exc, None, compact=True + ).format_exception_only() + ) + + +@format_exception_only.register +def _(__exc: type, value: BaseException) -> List[str]: + return format_exception_only(value) + + +@singledispatch +def format_exception( + __exc: BaseException, + limit: Optional[int] = None, + chain: bool = True, +) -> List[str]: + return list( + PatchedTracebackException( + type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True + ).format(chain=chain) + ) + + +@format_exception.register +def _( + __exc: type, + value: BaseException, + tb: TracebackType, + limit: Optional[int] = None, + chain: bool = True, +) -> List[str]: + return format_exception(value, limit, chain) + + +@singledispatch +def print_exception( + __exc: BaseException, + limit: Optional[int] = None, + file: Any = None, + chain: bool = True, +) -> None: + if file is None: + file = sys.stderr + + for line in PatchedTracebackException( + type(__exc), __exc, __exc.__traceback__, limit=limit + ).format(chain=chain): + print(line, file=file, end="") + + +@print_exception.register +def _( + __exc: type, + value: BaseException, + tb: TracebackType, + limit: Optional[int] = None, + file: Any = None, + chain: bool = True, +) -> None: + print_exception(value, limit, file, chain) + + +def print_exc( + limit: Optional[int] = None, + file: Any | None = None, + chain: bool = True, +) -> None: + value = sys.exc_info()[1] + print_exception(value, limit, file, chain) + + +# Python levenshtein edit distance code for NameError/AttributeError +# suggestions, backported from 3.12 + +_MAX_CANDIDATE_ITEMS = 750 +_MAX_STRING_SIZE = 40 +_MOVE_COST = 2 +_CASE_COST = 1 +_SENTINEL = object() + + +def _substitution_cost(ch_a, ch_b): + if ch_a == ch_b: + return 0 + if ch_a.lower() == ch_b.lower(): + return _CASE_COST + return _MOVE_COST + + +def _compute_suggestion_error(exc_value, tb): + wrong_name = getattr(exc_value, "name", None) + if wrong_name is None or not isinstance(wrong_name, str): + return None + if isinstance(exc_value, AttributeError): + obj = getattr(exc_value, "obj", _SENTINEL) + if obj is _SENTINEL: + return None + obj = exc_value.obj + try: + d = dir(obj) + except Exception: + return None + else: + assert isinstance(exc_value, NameError) + # find most recent frame + if tb is None: + return None + while tb.tb_next is not None: + tb = tb.tb_next + frame = tb.tb_frame + + d = list(frame.f_locals) + list(frame.f_globals) + list(frame.f_builtins) + if len(d) > _MAX_CANDIDATE_ITEMS: + return None + wrong_name_len = len(wrong_name) + if wrong_name_len > _MAX_STRING_SIZE: + return None + best_distance = wrong_name_len + suggestion = None + for possible_name in d: + if possible_name == wrong_name: + # A missing attribute is "found". Don't suggest it (see GH-88821). + continue + # No more than 1/3 of the involved characters should need changed. + max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6 + # Don't take matches we've already beaten. + max_distance = min(max_distance, best_distance - 1) + current_distance = _levenshtein_distance( + wrong_name, possible_name, max_distance + ) + if current_distance > max_distance: + continue + if not suggestion or current_distance < best_distance: + suggestion = possible_name + best_distance = current_distance + return suggestion + + +def _levenshtein_distance(a, b, max_cost): + # A Python implementation of Python/suggestions.c:levenshtein_distance. + + # Both strings are the same + if a == b: + return 0 + + # Trim away common affixes + pre = 0 + while a[pre:] and b[pre:] and a[pre] == b[pre]: + pre += 1 + a = a[pre:] + b = b[pre:] + post = 0 + while a[: post or None] and b[: post or None] and a[post - 1] == b[post - 1]: + post -= 1 + a = a[: post or None] + b = b[: post or None] + if not a or not b: + return _MOVE_COST * (len(a) + len(b)) + if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE: + return max_cost + 1 + + # Prefer shorter buffer + if len(b) < len(a): + a, b = b, a + + # Quick fail when a match is impossible + if (len(b) - len(a)) * _MOVE_COST > max_cost: + return max_cost + 1 + + # Instead of producing the whole traditional len(a)-by-len(b) + # matrix, we can update just one row in place. + # Initialize the buffer row + row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST)) + + result = 0 + for bindex in range(len(b)): + bchar = b[bindex] + distance = result = bindex * _MOVE_COST + minimum = sys.maxsize + for index in range(len(a)): + # 1) Previous distance in this row is cost(b[:b_index], a[:index]) + substitute = distance + _substitution_cost(bchar, a[index]) + # 2) cost(b[:b_index], a[:index+1]) from previous row + distance = row[index] + # 3) existing result is cost(b[:b_index+1], a[index]) + + insert_delete = min(result, distance) + _MOVE_COST + result = min(insert_delete, substitute) + + # cost(b[:b_index+1], a[:index+1]) + row[index] = result + if result < minimum: + minimum = result + if minimum > max_cost: + # Everything in this row is too big, so bail early. + return max_cost + 1 + return result diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_suppress.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_suppress.py new file mode 100644 index 00000000000..11467eeda9b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_suppress.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import sys +from contextlib import AbstractContextManager +from types import TracebackType +from typing import TYPE_CHECKING, Optional, Type, cast + +if sys.version_info < (3, 11): + from ._exceptions import BaseExceptionGroup + +if TYPE_CHECKING: + # requires python 3.9 + BaseClass = AbstractContextManager[None] +else: + BaseClass = AbstractContextManager + + +class suppress(BaseClass): + """Backport of :class:`contextlib.suppress` from Python 3.12.1.""" + + def __init__(self, *exceptions: type[BaseException]): + self._exceptions = exceptions + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exctype: Optional[Type[BaseException]], + excinst: Optional[BaseException], + exctb: Optional[TracebackType], + ) -> bool: + # Unlike isinstance and issubclass, CPython exception handling + # currently only looks at the concrete type hierarchy (ignoring + # the instance and subclass checking hooks). While Guido considers + # that a bug rather than a feature, it's a fairly hard one to fix + # due to various internal implementation details. suppress provides + # the simpler issubclass based semantics, rather than trying to + # exactly reproduce the limitations of the CPython interpreter. + # + # See http://bugs.python.org/issue12029 for more details + if exctype is None: + return False + + if issubclass(exctype, self._exceptions): + return True + + if issubclass(exctype, BaseExceptionGroup): + match, rest = cast(BaseExceptionGroup, excinst).split(self._exceptions) + if rest is None: + return True + + raise rest + + return False diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_version.py b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_version.py new file mode 100644 index 00000000000..9e1bb0b65ea --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/_version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '1.2.1' +__version_tuple__ = version_tuple = (1, 2, 1) diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/py.typed index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/py.typed +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/src/exceptiongroup/py.typed diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/__init__.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/__init__.py index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/__init__.py +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/__init__.py diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/apport_excepthook.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/apport_excepthook.py new file mode 100644 index 00000000000..1e4a8f30dee --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/apport_excepthook.py @@ -0,0 +1,13 @@ +# The apport_python_hook package is only installed as part of Ubuntu's system +# python, and not available in venvs. So before we can import it we have to +# make sure it's on sys.path. +import sys + +sys.path.append("/usr/lib/python3/dist-packages") +import apport_python_hook # unsorted import + +apport_python_hook.install() + +from exceptiongroup import ExceptionGroup # noqa: E402 # unsorted import + +raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")]) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/check_types.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/check_types.py new file mode 100644 index 00000000000..f7a102d5d8a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/check_types.py @@ -0,0 +1,36 @@ +from typing_extensions import assert_type + +from exceptiongroup import BaseExceptionGroup, ExceptionGroup, catch, suppress + +# issue 117 +a = BaseExceptionGroup("", (KeyboardInterrupt(),)) +assert_type(a, BaseExceptionGroup[KeyboardInterrupt]) +b = BaseExceptionGroup("", (ValueError(),)) +assert_type(b, BaseExceptionGroup[ValueError]) +c = ExceptionGroup("", (ValueError(),)) +assert_type(c, ExceptionGroup[ValueError]) + +# expected type error when passing a BaseException to ExceptionGroup +ExceptionGroup("", (KeyboardInterrupt(),)) # type: ignore[type-var] + + +# code snippets from the README + + +def value_key_err_handler(excgroup: BaseExceptionGroup) -> None: + for exc in excgroup.exceptions: + print("Caught exception:", type(exc)) + + +def runtime_err_handler(exc: BaseExceptionGroup) -> None: + print("Caught runtime error") + + +with catch( + {(ValueError, KeyError): value_key_err_handler, RuntimeError: runtime_err_handler} +): + ... + + +with suppress(RuntimeError): + raise ExceptionGroup("", [RuntimeError("boo")]) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/conftest.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/conftest.py new file mode 100644 index 00000000000..aeca72d3b5e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/conftest.py @@ -0,0 +1,4 @@ +import sys + +if sys.version_info < (3, 11): + collect_ignore_glob = ["*_py311.py"] diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_apport_monkeypatching.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_apport_monkeypatching.py new file mode 100644 index 00000000000..998554fde69 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_apport_monkeypatching.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +import exceptiongroup + + +def run_script(name: str) -> subprocess.CompletedProcess[bytes]: + exceptiongroup_path = Path(exceptiongroup.__file__).parent.parent + script_path = Path(__file__).parent / name + + env = dict(os.environ) + print("parent PYTHONPATH:", env.get("PYTHONPATH")) + if "PYTHONPATH" in env: # pragma: no cover + pp = env["PYTHONPATH"].split(os.pathsep) + else: + pp = [] + + pp.insert(0, str(exceptiongroup_path)) + pp.insert(0, str(script_path.parent)) + env["PYTHONPATH"] = os.pathsep.join(pp) + print("subprocess PYTHONPATH:", env.get("PYTHONPATH")) + + cmd = [sys.executable, "-u", str(script_path)] + print("running:", cmd) + completed = subprocess.run( + cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + print("process output:") + print(completed.stdout.decode("utf-8")) + return completed + + +@pytest.mark.skipif( + sys.version_info > (3, 11), + reason="No patching is done on Python >= 3.11", +) +@pytest.mark.skipif( + not Path("/usr/lib/python3/dist-packages/apport_python_hook.py").exists(), + reason="need Ubuntu with python3-apport installed", +) +def test_apport_excepthook_monkeypatch_interaction(): + completed = run_script("apport_excepthook.py") + stdout = completed.stdout.decode("utf-8") + file = Path(__file__).parent / "apport_excepthook.py" + assert stdout == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{file}", line 13, in <module> + | raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")]) + | exceptiongroup.ExceptionGroup: msg1 (2 sub-exceptions) + +-+---------------- 1 ---------------- + | KeyError: 'msg2' + +---------------- 2 ---------------- + | ValueError: msg3 + +------------------------------------ +""" + ) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch.py new file mode 100644 index 00000000000..1da749e23c6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch.py @@ -0,0 +1,222 @@ +import pytest + +from exceptiongroup import BaseExceptionGroup, ExceptionGroup, catch + + +def test_bad_arg(): + with pytest.raises(TypeError, match="the argument must be a mapping"): + with catch(1): + pass + + +def test_bad_handler(): + with pytest.raises(TypeError, match="handlers must be callable"): + with catch({RuntimeError: None}): + pass + + +@pytest.mark.parametrize( + "exc_type", + [ + pytest.param(BaseExceptionGroup, id="naked_basegroup"), + pytest.param(ExceptionGroup, id="naked_group"), + pytest.param((ValueError, BaseExceptionGroup), id="iterable_basegroup"), + pytest.param((ValueError, ExceptionGroup), id="iterable_group"), + ], +) +def test_catch_exceptiongroup(exc_type): + with pytest.raises(TypeError, match="catching ExceptionGroup with catch"): + with catch({exc_type: (lambda e: True)}): + pass + + +def test_catch_ungrouped(): + value_type_errors = [] + zero_division_errors = [] + for exc in [ValueError("foo"), TypeError("bar"), ZeroDivisionError()]: + with catch( + { + (ValueError, TypeError): value_type_errors.append, + ZeroDivisionError: zero_division_errors.append, + } + ): + raise exc + + assert len(value_type_errors) == 2 + + assert isinstance(value_type_errors[0], ExceptionGroup) + assert len(value_type_errors[0].exceptions) == 1 + assert isinstance(value_type_errors[0].exceptions[0], ValueError) + + assert isinstance(value_type_errors[1], ExceptionGroup) + assert len(value_type_errors[1].exceptions) == 1 + assert isinstance(value_type_errors[1].exceptions[0], TypeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + assert isinstance(zero_division_errors[0].exceptions[0], ZeroDivisionError) + assert len(zero_division_errors[0].exceptions) == 1 + + +def test_catch_group(): + value_runtime_errors = [] + zero_division_errors = [] + with catch( + { + (ValueError, RuntimeError): value_runtime_errors.append, + ZeroDivisionError: zero_division_errors.append, + } + ): + raise ExceptionGroup( + "booboo", + [ + ValueError("foo"), + ValueError("bar"), + RuntimeError("bar"), + ZeroDivisionError(), + ], + ) + + assert len(value_runtime_errors) == 1 + assert isinstance(value_runtime_errors[0], ExceptionGroup) + exceptions = value_runtime_errors[0].exceptions + assert isinstance(exceptions[0], ValueError) + assert isinstance(exceptions[1], ValueError) + assert isinstance(exceptions[2], RuntimeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + exceptions = zero_division_errors[0].exceptions + assert isinstance(exceptions[0], ZeroDivisionError) + + +def test_catch_nested_group(): + value_runtime_errors = [] + zero_division_errors = [] + with catch( + { + (ValueError, RuntimeError): value_runtime_errors.append, + ZeroDivisionError: zero_division_errors.append, + } + ): + nested_group = ExceptionGroup( + "nested", [RuntimeError("bar"), ZeroDivisionError()] + ) + raise ExceptionGroup("booboo", [ValueError("foo"), nested_group]) + + assert len(value_runtime_errors) == 1 + exceptions = value_runtime_errors[0].exceptions + assert isinstance(exceptions[0], ValueError) + assert isinstance(exceptions[1], ExceptionGroup) + assert isinstance(exceptions[1].exceptions[0], RuntimeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + assert isinstance(zero_division_errors[0].exceptions[0], ExceptionGroup) + assert isinstance( + zero_division_errors[0].exceptions[0].exceptions[0], ZeroDivisionError + ) + + +def test_catch_no_match(): + try: + with catch({(ValueError, RuntimeError): (lambda e: None)}): + group = ExceptionGroup("booboo", [ZeroDivisionError()]) + raise group + except ExceptionGroup as exc: + assert exc is not group + else: + pytest.fail("Did not raise an ExceptionGroup") + + +def test_catch_single_no_match(): + try: + with catch({(ValueError, RuntimeError): (lambda e: None)}): + raise ZeroDivisionError + except ZeroDivisionError: + pass + else: + pytest.fail("Did not raise an ZeroDivisionError") + + +def test_catch_full_match(): + with catch({(ValueError, RuntimeError): (lambda e: None)}): + raise ExceptionGroup("booboo", [ValueError()]) + + +def test_catch_handler_raises(): + def handler(exc): + raise RuntimeError("new") + + with pytest.raises(RuntimeError, match="new") as exc: + with catch({(ValueError, ValueError): handler}): + excgrp = ExceptionGroup("booboo", [ValueError("bar")]) + raise excgrp + + context = exc.value.__context__ + assert isinstance(context, ExceptionGroup) + assert str(context) == "booboo (1 sub-exception)" + assert len(context.exceptions) == 1 + assert isinstance(context.exceptions[0], ValueError) + assert exc.value.__cause__ is None + + +def test_bare_raise_in_handler(): + """Test that a bare "raise" "middle" ecxeption group gets discarded.""" + + def handler(exc): + raise + + with pytest.raises(ExceptionGroup) as excgrp: + with catch({(ValueError,): handler, (RuntimeError,): lambda eg: None}): + try: + first_exc = RuntimeError("first") + raise first_exc + except RuntimeError as exc: + middle_exc = ExceptionGroup( + "bad", [ValueError(), ValueError(), TypeError()] + ) + raise middle_exc from exc + + assert len(excgrp.value.exceptions) == 2 + assert all(isinstance(exc, ValueError) for exc in excgrp.value.exceptions) + assert excgrp.value is not middle_exc + assert excgrp.value.__cause__ is first_exc + assert excgrp.value.__context__ is first_exc + + +def test_catch_subclass(): + lookup_errors = [] + with catch({LookupError: lookup_errors.append}): + raise KeyError("foo") + + assert len(lookup_errors) == 1 + assert isinstance(lookup_errors[0], ExceptionGroup) + exceptions = lookup_errors[0].exceptions + assert isinstance(exceptions[0], KeyError) + + +def test_async_handler(request): + async def handler(eg): + pass + + def delegate(eg): + coro = handler(eg) + request.addfinalizer(coro.close) + return coro + + with pytest.raises(TypeError, match="Exception handler must be a sync function."): + with catch({TypeError: delegate}): + raise ExceptionGroup("message", [TypeError("uh-oh")]) + + +def test_bare_reraise_from_naked_exception(): + def handler(eg): + raise + + with pytest.raises(ExceptionGroup) as excgrp, catch({Exception: handler}): + raise KeyError("foo") + + assert len(excgrp.value.exceptions) == 1 + assert isinstance(excgrp.value.exceptions[0], KeyError) + assert str(excgrp.value.exceptions[0]) == "'foo'" diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch_py311.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch_py311.py new file mode 100644 index 00000000000..2f12ac32a28 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_catch_py311.py @@ -0,0 +1,190 @@ +import sys + +import pytest + +from exceptiongroup import ExceptionGroup + + +def test_catch_ungrouped(): + value_type_errors = [] + zero_division_errors = [] + for exc in [ValueError("foo"), TypeError("bar"), ZeroDivisionError()]: + try: + raise exc + except* (ValueError, TypeError) as e: + value_type_errors.append(e) + except* ZeroDivisionError as e: + zero_division_errors.append(e) + + assert len(value_type_errors) == 2 + + assert isinstance(value_type_errors[0], ExceptionGroup) + assert len(value_type_errors[0].exceptions) == 1 + assert isinstance(value_type_errors[0].exceptions[0], ValueError) + + assert isinstance(value_type_errors[1], ExceptionGroup) + assert len(value_type_errors[1].exceptions) == 1 + assert isinstance(value_type_errors[1].exceptions[0], TypeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + assert isinstance(zero_division_errors[0].exceptions[0], ZeroDivisionError) + assert len(zero_division_errors[0].exceptions) == 1 + + +def test_catch_group(): + value_runtime_errors = [] + zero_division_errors = [] + try: + raise ExceptionGroup( + "booboo", + [ + ValueError("foo"), + ValueError("bar"), + RuntimeError("bar"), + ZeroDivisionError(), + ], + ) + except* (ValueError, RuntimeError) as exc: + value_runtime_errors.append(exc) + except* ZeroDivisionError as exc: + zero_division_errors.append(exc) + + assert len(value_runtime_errors) == 1 + assert isinstance(value_runtime_errors[0], ExceptionGroup) + exceptions = value_runtime_errors[0].exceptions + assert isinstance(exceptions[0], ValueError) + assert isinstance(exceptions[1], ValueError) + assert isinstance(exceptions[2], RuntimeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + exceptions = zero_division_errors[0].exceptions + assert isinstance(exceptions[0], ZeroDivisionError) + + +def test_catch_nested_group(): + value_runtime_errors = [] + zero_division_errors = [] + try: + nested_group = ExceptionGroup( + "nested", [RuntimeError("bar"), ZeroDivisionError()] + ) + raise ExceptionGroup("booboo", [ValueError("foo"), nested_group]) + except* (ValueError, RuntimeError) as exc: + value_runtime_errors.append(exc) + except* ZeroDivisionError as exc: + zero_division_errors.append(exc) + + assert len(value_runtime_errors) == 1 + exceptions = value_runtime_errors[0].exceptions + assert isinstance(exceptions[0], ValueError) + assert isinstance(exceptions[1], ExceptionGroup) + assert isinstance(exceptions[1].exceptions[0], RuntimeError) + + assert len(zero_division_errors) == 1 + assert isinstance(zero_division_errors[0], ExceptionGroup) + assert isinstance(zero_division_errors[0].exceptions[0], ExceptionGroup) + assert isinstance( + zero_division_errors[0].exceptions[0].exceptions[0], ZeroDivisionError + ) + + +def test_catch_no_match(): + try: + try: + group = ExceptionGroup("booboo", [ZeroDivisionError()]) + raise group + except* (ValueError, RuntimeError): + pass + except ExceptionGroup as exc: + assert isinstance(exc.exceptions[0], ZeroDivisionError) + assert exc is not group + else: + pytest.fail("Did not raise an ExceptionGroup") + + +def test_catch_single_no_match(): + try: + try: + raise ZeroDivisionError + except* (ValueError, RuntimeError): + pass + except ZeroDivisionError: + pass + else: + pytest.fail("Did not raise an ZeroDivisionError") + + +def test_catch_full_match(): + try: + raise ExceptionGroup("booboo", [ValueError()]) + except* (ValueError, RuntimeError): + pass + + +@pytest.mark.skipif( + sys.version_info < (3, 11, 4), + reason="Behavior was changed in 3.11.4", +) +def test_catch_handler_raises(): + with pytest.raises(RuntimeError, match="new") as exc: + try: + excgrp = ExceptionGroup("booboo", [ValueError("bar")]) + raise excgrp + except* ValueError: + raise RuntimeError("new") + + context = exc.value.__context__ + assert isinstance(context, ExceptionGroup) + assert str(context) == "booboo (1 sub-exception)" + assert len(context.exceptions) == 1 + assert isinstance(context.exceptions[0], ValueError) + assert exc.value.__cause__ is None + + +def test_catch_subclass(): + lookup_errors = [] + try: + raise KeyError("foo") + except* LookupError as e: + lookup_errors.append(e) + + assert len(lookup_errors) == 1 + assert isinstance(lookup_errors[0], ExceptionGroup) + exceptions = lookup_errors[0].exceptions + assert isinstance(exceptions[0], KeyError) + + +def test_bare_raise_in_handler(): + """Test that the "middle" ecxeption group gets discarded.""" + with pytest.raises(ExceptionGroup) as excgrp: + try: + try: + first_exc = RuntimeError("first") + raise first_exc + except RuntimeError as exc: + middle_exc = ExceptionGroup( + "bad", [ValueError(), ValueError(), TypeError()] + ) + raise middle_exc from exc + except* ValueError: + raise + except* TypeError: + pass + + assert excgrp.value is not middle_exc + assert excgrp.value.__cause__ is first_exc + assert excgrp.value.__context__ is first_exc + + +def test_bare_reraise_from_naked_exception(): + with pytest.raises(ExceptionGroup) as excgrp: + try: + raise KeyError("foo") + except* KeyError: + raise + + assert len(excgrp.value.exceptions) == 1 + assert isinstance(excgrp.value.exceptions[0], KeyError) + assert str(excgrp.value.exceptions[0]) == "'foo'" diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_exceptions.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_exceptions.py new file mode 100644 index 00000000000..f77511ab61e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_exceptions.py @@ -0,0 +1,845 @@ +# Copied from the standard library +import collections.abc +import sys +import unittest + +import pytest + +from exceptiongroup import BaseExceptionGroup, ExceptionGroup + + +class TestExceptionGroupTypeHierarchy(unittest.TestCase): + def test_exception_group_types(self): + self.assertTrue(issubclass(ExceptionGroup, Exception)) + self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup)) + self.assertTrue(issubclass(BaseExceptionGroup, BaseException)) + + def test_exception_group_is_generic_type(self): + E = OSError + self.assertEqual(ExceptionGroup[E].__origin__, ExceptionGroup) + self.assertEqual(BaseExceptionGroup[E].__origin__, BaseExceptionGroup) + + +class BadConstructorArgs(unittest.TestCase): + def test_bad_EG_construction__too_few_args(self): + if sys.version_info >= (3, 11): + MSG = ( + r"BaseExceptionGroup.__new__\(\) takes exactly 2 arguments \(1 given\)" + ) + else: + MSG = ( + r"__new__\(\) missing 1 required positional argument: " + r"'_ExceptionGroup__exceptions'" + ) + + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("no errors") + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup([ValueError("no msg")]) + + def test_bad_EG_construction__too_many_args(self): + if sys.version_info >= (3, 11): + MSG = ( + r"BaseExceptionGroup.__new__\(\) takes exactly 2 arguments \(3 given\)" + ) + else: + MSG = r"__new__\(\) takes 3 positional arguments but 4 were given" + + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("eg", [ValueError("too")], [TypeError("many")]) + + def test_bad_EG_construction__bad_message(self): + MSG = "argument 1 must be str, not " + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup(ValueError(12), SyntaxError("bad syntax")) + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup(None, [ValueError(12)]) + + def test_bad_EG_construction__bad_excs_sequence(self): + MSG = r"second argument \(exceptions\) must be a sequence" + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("errors not sequence", {ValueError(42)}) + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("eg", None) + + MSG = r"second argument \(exceptions\) must be a non-empty sequence" + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup("eg", []) + + def test_bad_EG_construction__nested_non_exceptions(self): + MSG = r"Item [0-9]+ of second argument \(exceptions\) is not an exception" + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup("expect instance, not type", [OSError]) + with self.assertRaisesRegex(ValueError, MSG): + ExceptionGroup("bad error", ["not an exception"]) + + +class InstanceCreation(unittest.TestCase): + def test_EG_wraps_Exceptions__creates_EG(self): + excs = [ValueError(1), TypeError(2)] + self.assertIs(type(ExceptionGroup("eg", excs)), ExceptionGroup) + + def test_BEG_wraps_Exceptions__creates_EG(self): + excs = [ValueError(1), TypeError(2)] + self.assertIs(type(BaseExceptionGroup("beg", excs)), ExceptionGroup) + + def test_EG_wraps_BaseException__raises_TypeError(self): + MSG = "Cannot nest BaseExceptions in an ExceptionGroup" + with self.assertRaisesRegex(TypeError, MSG): + ExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)]) + + def test_BEG_wraps_BaseException__creates_BEG(self): + beg = BaseExceptionGroup("beg", [ValueError(1), KeyboardInterrupt(2)]) + self.assertIs(type(beg), BaseExceptionGroup) + + def test_EG_subclass_wraps_non_base_exceptions(self): + class MyEG(ExceptionGroup): + pass + + self.assertIs(type(MyEG("eg", [ValueError(12), TypeError(42)])), MyEG) + + @pytest.mark.skipif( + sys.version_info[:3] == (3, 11, 0), + reason="Behavior was made stricter in 3.11.1", + ) + def test_EG_subclass_does_not_wrap_base_exceptions(self): + class MyEG(ExceptionGroup): + pass + + msg = "Cannot nest BaseExceptions in 'MyEG'" + with self.assertRaisesRegex(TypeError, msg): + MyEG("eg", [ValueError(12), KeyboardInterrupt(42)]) + + @pytest.mark.skipif( + sys.version_info[:3] == (3, 11, 0), + reason="Behavior was made stricter in 3.11.1", + ) + def test_BEG_and_E_subclass_does_not_wrap_base_exceptions(self): + class MyEG(BaseExceptionGroup, ValueError): + pass + + msg = "Cannot nest BaseExceptions in 'MyEG'" + with self.assertRaisesRegex(TypeError, msg): + MyEG("eg", [ValueError(12), KeyboardInterrupt(42)]) + + +def create_simple_eg(): + excs = [] + try: + try: + raise MemoryError("context and cause for ValueError(1)") + except MemoryError as e: + raise ValueError(1) from e + except ValueError as e: + excs.append(e) + + try: + try: + raise OSError("context for TypeError") + except OSError: + raise TypeError(int) + except TypeError as e: + excs.append(e) + + try: + try: + raise ImportError("context for ValueError(2)") + except ImportError: + raise ValueError(2) + except ValueError as e: + excs.append(e) + + try: + raise ExceptionGroup("simple eg", excs) + except ExceptionGroup as e: + return e + + +class ExceptionGroupFields(unittest.TestCase): + def test_basics_ExceptionGroup_fields(self): + eg = create_simple_eg() + + # check msg + self.assertEqual(eg.message, "simple eg") + self.assertEqual(eg.args[0], "simple eg") + + # check cause and context + self.assertIsInstance(eg.exceptions[0], ValueError) + self.assertIsInstance(eg.exceptions[0].__cause__, MemoryError) + self.assertIsInstance(eg.exceptions[0].__context__, MemoryError) + self.assertIsInstance(eg.exceptions[1], TypeError) + self.assertIsNone(eg.exceptions[1].__cause__) + self.assertIsInstance(eg.exceptions[1].__context__, OSError) + self.assertIsInstance(eg.exceptions[2], ValueError) + self.assertIsNone(eg.exceptions[2].__cause__) + self.assertIsInstance(eg.exceptions[2].__context__, ImportError) + + # check tracebacks + line0 = create_simple_eg.__code__.co_firstlineno + tb_linenos = [line0 + 27, [line0 + 6, line0 + 14, line0 + 22]] + self.assertEqual(eg.__traceback__.tb_lineno, tb_linenos[0]) + self.assertIsNone(eg.__traceback__.tb_next) + for i in range(3): + tb = eg.exceptions[i].__traceback__ + self.assertIsNone(tb.tb_next) + self.assertEqual(tb.tb_lineno, tb_linenos[1][i]) + + def test_fields_are_readonly(self): + eg = ExceptionGroup("eg", [TypeError(1), OSError(2)]) + + self.assertEqual(type(eg.exceptions), tuple) + + eg.message + with self.assertRaises(AttributeError): + eg.message = "new msg" + + eg.exceptions + with self.assertRaises(AttributeError): + eg.exceptions = [OSError("xyz")] + + def test_notes_is_list_of_strings_if_it_exists(self): + eg = create_simple_eg() + + note = "This is a happy note for the exception group" + self.assertFalse(hasattr(eg, "__notes__")) + eg.add_note(note) + self.assertEqual(eg.__notes__, [note]) + + def test_derive_doesn_copy_notes(self): + eg = create_simple_eg() + eg.add_note("hello") + assert eg.__notes__ == ["hello"] + eg2 = eg.derive([ValueError()]) + assert not hasattr(eg2, "__notes__") + + +class ExceptionGroupTestBase(unittest.TestCase): + def assertMatchesTemplate(self, exc, exc_type, template): + """Assert that the exception matches the template + + A template describes the shape of exc. If exc is a + leaf exception (i.e., not an exception group) then + template is an exception instance that has the + expected type and args value of exc. If exc is an + exception group, then template is a list of the + templates of its nested exceptions. + """ + if exc_type is not None: + self.assertIs(type(exc), exc_type) + + if isinstance(exc, BaseExceptionGroup): + self.assertIsInstance(template, collections.abc.Sequence) + self.assertEqual(len(exc.exceptions), len(template)) + for e, t in zip(exc.exceptions, template): + self.assertMatchesTemplate(e, None, t) + else: + self.assertIsInstance(template, BaseException) + self.assertEqual(type(exc), type(template)) + self.assertEqual(exc.args, template.args) + + +class ExceptionGroupSubgroupTests(ExceptionGroupTestBase): + def setUp(self): + self.eg = create_simple_eg() + self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] + + def test_basics_subgroup_split__bad_arg_type(self): + bad_args = [ + "bad arg", + OSError("instance not type"), + [OSError, TypeError], + (OSError, 42), + ] + for arg in bad_args: + with self.assertRaises(TypeError): + self.eg.subgroup(arg) + with self.assertRaises(TypeError): + self.eg.split(arg) + + def test_basics_subgroup_by_type__passthrough(self): + eg = self.eg + # self.assertIs(eg, eg.subgroup(BaseException)) + # self.assertIs(eg, eg.subgroup(Exception)) + self.assertIs(eg, eg.subgroup(BaseExceptionGroup)) + self.assertIs(eg, eg.subgroup(ExceptionGroup)) + + def test_basics_subgroup_by_type__no_match(self): + self.assertIsNone(self.eg.subgroup(OSError)) + + def test_basics_subgroup_by_type__match(self): + eg = self.eg + testcases = [ + # (match_type, result_template) + (ValueError, [ValueError(1), ValueError(2)]), + (TypeError, [TypeError(int)]), + ((ValueError, TypeError), self.eg_template), + ] + + for match_type, template in testcases: + with self.subTest(match=match_type): + subeg = eg.subgroup(match_type) + self.assertEqual(subeg.message, eg.message) + self.assertMatchesTemplate(subeg, ExceptionGroup, template) + + def test_basics_subgroup_by_predicate__passthrough(self): + self.assertIs(self.eg, self.eg.subgroup(lambda e: True)) + + def test_basics_subgroup_by_predicate__no_match(self): + self.assertIsNone(self.eg.subgroup(lambda e: False)) + + def test_basics_subgroup_by_predicate__match(self): + eg = self.eg + testcases = [ + # (match_type, result_template) + (ValueError, [ValueError(1), ValueError(2)]), + (TypeError, [TypeError(int)]), + ((ValueError, TypeError), self.eg_template), + ] + + for match_type, template in testcases: + subeg = eg.subgroup(lambda e: isinstance(e, match_type)) + self.assertEqual(subeg.message, eg.message) + self.assertMatchesTemplate(subeg, ExceptionGroup, template) + + +class ExceptionGroupSplitTests(ExceptionGroupTestBase): + def setUp(self): + self.eg = create_simple_eg() + self.eg_template = [ValueError(1), TypeError(int), ValueError(2)] + + def test_basics_split_by_type__passthrough(self): + for E in [BaseException, Exception, BaseExceptionGroup, ExceptionGroup]: + match, rest = self.eg.split(E) + self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template) + self.assertIsNone(rest) + + def test_basics_split_by_type__no_match(self): + match, rest = self.eg.split(OSError) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template) + + def test_basics_split_by_type__match(self): + eg = self.eg + VE = ValueError + TE = TypeError + testcases = [ + # (matcher, match_template, rest_template) + (VE, [VE(1), VE(2)], [TE(int)]), + (TE, [TE(int)], [VE(1), VE(2)]), + ((VE, TE), self.eg_template, None), + ((OSError, VE), [VE(1), VE(2)], [TE(int)]), + ] + + for match_type, match_template, rest_template in testcases: + match, rest = eg.split(match_type) + self.assertEqual(match.message, eg.message) + self.assertMatchesTemplate(match, ExceptionGroup, match_template) + if rest_template is not None: + self.assertEqual(rest.message, eg.message) + self.assertMatchesTemplate(rest, ExceptionGroup, rest_template) + else: + self.assertIsNone(rest) + + def test_basics_split_by_predicate__passthrough(self): + match, rest = self.eg.split(lambda e: True) + self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template) + self.assertIsNone(rest) + + def test_basics_split_by_predicate__no_match(self): + match, rest = self.eg.split(lambda e: False) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template) + + def test_basics_split_by_predicate__match(self): + eg = self.eg + VE = ValueError + TE = TypeError + testcases = [ + # (matcher, match_template, rest_template) + (VE, [VE(1), VE(2)], [TE(int)]), + (TE, [TE(int)], [VE(1), VE(2)]), + ((VE, TE), self.eg_template, None), + ] + + for match_type, match_template, rest_template in testcases: + match, rest = eg.split(lambda e: isinstance(e, match_type)) + self.assertEqual(match.message, eg.message) + self.assertMatchesTemplate(match, ExceptionGroup, match_template) + if rest_template is not None: + self.assertEqual(rest.message, eg.message) + self.assertMatchesTemplate(rest, ExceptionGroup, rest_template) + + +class DeepRecursionInSplitAndSubgroup(unittest.TestCase): + def make_deep_eg(self): + e = TypeError(1) + for _ in range(10000): + e = ExceptionGroup("eg", [e]) + return e + + def test_deep_split(self): + e = self.make_deep_eg() + with self.assertRaises(RecursionError): + e.split(TypeError) + + def test_deep_subgroup(self): + e = self.make_deep_eg() + with self.assertRaises(RecursionError): + e.subgroup(TypeError) + + +def leaf_generator(exc, tbs=None): + if tbs is None: + tbs = [] + tbs.append(exc.__traceback__) + if isinstance(exc, BaseExceptionGroup): + for e in exc.exceptions: + yield from leaf_generator(e, tbs) + else: + # exc is a leaf exception and its traceback + # is the concatenation of the traceback + # segments in tbs + yield exc, tbs + tbs.pop() + + +class LeafGeneratorTest(unittest.TestCase): + # The leaf_generator is mentioned in PEP 654 as a suggestion + # on how to iterate over leaf nodes of an EG. Is is also + # used below as a test utility. So we test it here. + + def test_leaf_generator(self): + eg = create_simple_eg() + + self.assertSequenceEqual([e for e, _ in leaf_generator(eg)], eg.exceptions) + + for e, tbs in leaf_generator(eg): + self.assertSequenceEqual(tbs, [eg.__traceback__, e.__traceback__]) + + +def create_nested_eg(): + excs = [] + try: + try: + raise TypeError(bytes) + except TypeError as e: + raise ExceptionGroup("nested", [e]) + except ExceptionGroup as e: + excs.append(e) + + try: + try: + raise MemoryError("out of memory") + except MemoryError as e: + raise ValueError(1) from e + except ValueError as e: + excs.append(e) + + try: + raise ExceptionGroup("root", excs) + except ExceptionGroup as eg: + return eg + + +class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase): + def test_nested_group_matches_template(self): + eg = create_nested_eg() + self.assertMatchesTemplate( + eg, ExceptionGroup, [[TypeError(bytes)], ValueError(1)] + ) + + def test_nested_group_chaining(self): + eg = create_nested_eg() + self.assertIsInstance(eg.exceptions[1].__context__, MemoryError) + self.assertIsInstance(eg.exceptions[1].__cause__, MemoryError) + self.assertIsInstance(eg.exceptions[0].__context__, TypeError) + + def test_nested_exception_group_tracebacks(self): + eg = create_nested_eg() + + line0 = create_nested_eg.__code__.co_firstlineno + for tb, expected in [ + (eg.__traceback__, line0 + 19), + (eg.exceptions[0].__traceback__, line0 + 6), + (eg.exceptions[1].__traceback__, line0 + 14), + (eg.exceptions[0].exceptions[0].__traceback__, line0 + 4), + ]: + self.assertEqual(tb.tb_lineno, expected) + self.assertIsNone(tb.tb_next) + + def test_iteration_full_tracebacks(self): + eg = create_nested_eg() + # check that iteration over leaves + # produces the expected tracebacks + self.assertEqual(len(list(leaf_generator(eg))), 2) + + line0 = create_nested_eg.__code__.co_firstlineno + expected_tbs = [[line0 + 19, line0 + 6, line0 + 4], [line0 + 19, line0 + 14]] + + for i, (_, tbs) in enumerate(leaf_generator(eg)): + self.assertSequenceEqual([tb.tb_lineno for tb in tbs], expected_tbs[i]) + + +class ExceptionGroupSplitTestBase(ExceptionGroupTestBase): + def split_exception_group(self, eg, types): + """Split an EG and do some sanity checks on the result""" + self.assertIsInstance(eg, BaseExceptionGroup) + + match, rest = eg.split(types) + sg = eg.subgroup(types) + + if match is not None: + self.assertIsInstance(match, BaseExceptionGroup) + for e, _ in leaf_generator(match): + self.assertIsInstance(e, types) + + self.assertIsNotNone(sg) + self.assertIsInstance(sg, BaseExceptionGroup) + for e, _ in leaf_generator(sg): + self.assertIsInstance(e, types) + + if rest is not None: + self.assertIsInstance(rest, BaseExceptionGroup) + + def leaves(exc): + return [] if exc is None else [e for e, _ in leaf_generator(exc)] + + # match and subgroup have the same leaves + self.assertSequenceEqual(leaves(match), leaves(sg)) + + match_leaves = leaves(match) + rest_leaves = leaves(rest) + # each leaf exception of eg is in exactly one of match and rest + self.assertEqual(len(leaves(eg)), len(leaves(match)) + len(leaves(rest))) + + for e in leaves(eg): + self.assertNotEqual(match and e in match_leaves, rest and e in rest_leaves) + + # message, cause and context, traceback and note equal to eg + for part in [match, rest, sg]: + if part is not None: + self.assertEqual(eg.message, part.message) + self.assertIs(eg.__cause__, part.__cause__) + self.assertIs(eg.__context__, part.__context__) + self.assertIs(eg.__traceback__, part.__traceback__) + self.assertEqual( + getattr(eg, "__notes__", None), + getattr(part, "__notes__", None), + ) + + def tbs_for_leaf(leaf, eg): + for e, tbs in leaf_generator(eg): + if e is leaf: + return tbs + + def tb_linenos(tbs): + return [tb.tb_lineno for tb in tbs if tb] + + # full tracebacks match + for part in [match, rest, sg]: + for e in leaves(part): + self.assertSequenceEqual( + tb_linenos(tbs_for_leaf(e, eg)), tb_linenos(tbs_for_leaf(e, part)) + ) + + return match, rest + + +class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase): + def test_split_by_type(self): + class MyExceptionGroup(ExceptionGroup): + pass + + def raiseVE(v): + raise ValueError(v) + + def raiseTE(t): + raise TypeError(t) + + def nested_group(): + def level1(i): + excs = [] + for f, arg in [(raiseVE, i), (raiseTE, int), (raiseVE, i + 1)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise ExceptionGroup("msg1", excs) + + def level2(i): + excs = [] + for f, arg in [(level1, i), (level1, i + 1), (raiseVE, i + 2)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise MyExceptionGroup("msg2", excs) + + def level3(i): + excs = [] + for f, arg in [(level2, i + 1), (raiseVE, i + 2)]: + try: + f(arg) + except Exception as e: + excs.append(e) + raise ExceptionGroup("msg3", excs) + + level3(5) + + try: + nested_group() + except ExceptionGroup as e: + e.add_note(f"the note: {id(e)}") + eg = e + + eg_template = [ + [ + [ValueError(6), TypeError(int), ValueError(7)], + [ValueError(7), TypeError(int), ValueError(8)], + ValueError(8), + ], + ValueError(7), + ] + + valueErrors_template = [ + [ + [ValueError(6), ValueError(7)], + [ValueError(7), ValueError(8)], + ValueError(8), + ], + ValueError(7), + ] + + typeErrors_template = [[[TypeError(int)], [TypeError(int)]]] + + self.assertMatchesTemplate(eg, ExceptionGroup, eg_template) + + # Match Nothing + match, rest = self.split_exception_group(eg, SyntaxError) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, ExceptionGroup, eg_template) + + # Match Everything + match, rest = self.split_exception_group(eg, BaseException) + self.assertMatchesTemplate(match, ExceptionGroup, eg_template) + self.assertIsNone(rest) + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(match, ExceptionGroup, eg_template) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, valueErrors_template) + self.assertMatchesTemplate(rest, ExceptionGroup, typeErrors_template) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, (TypeError, SyntaxError)) + self.assertMatchesTemplate(match, ExceptionGroup, typeErrors_template) + self.assertMatchesTemplate(rest, ExceptionGroup, valueErrors_template) + + # Match ExceptionGroup + match, rest = eg.split(ExceptionGroup) + self.assertIs(match, eg) + self.assertIsNone(rest) + + # Match MyExceptionGroup (ExceptionGroup subclass) + match, rest = eg.split(MyExceptionGroup) + self.assertMatchesTemplate(match, ExceptionGroup, [eg_template[0]]) + self.assertMatchesTemplate(rest, ExceptionGroup, [eg_template[1]]) + + def test_split_BaseExceptionGroup(self): + def exc(ex): + try: + raise ex + except BaseException as e: + return e + + try: + raise BaseExceptionGroup( + "beg", [exc(ValueError(1)), exc(KeyboardInterrupt(2))] + ) + except BaseExceptionGroup as e: + beg = e + + # Match Nothing + match, rest = self.split_exception_group(beg, TypeError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)] + ) + + # Match Everything + match, rest = self.split_exception_group(beg, (ValueError, KeyboardInterrupt)) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)] + ) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(beg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate(rest, BaseExceptionGroup, [KeyboardInterrupt(2)]) + + # Match KeyboardInterrupts + match, rest = self.split_exception_group(beg, KeyboardInterrupt) + self.assertMatchesTemplate(match, BaseExceptionGroup, [KeyboardInterrupt(2)]) + self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) + + +class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase): + def test_split_ExceptionGroup_subclass_no_derive_no_new_override(self): + class EG(ExceptionGroup): + pass + + try: + try: + try: + raise TypeError(2) + except TypeError as te: + raise EG("nested", [te]) + except EG as nested: + try: + raise ValueError(1) + except ValueError as ve: + raise EG("eg", [ve, nested]) + except EG as e: + eg = e + + self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, ExceptionGroup, [ValueError(1), [TypeError(2)]] + ) + + # Match Everything + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate( + match, ExceptionGroup, [ValueError(1), [TypeError(2)]] + ) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate(rest, ExceptionGroup, [[TypeError(2)]]) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, TypeError) + self.assertMatchesTemplate(match, ExceptionGroup, [[TypeError(2)]]) + self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) + + def test_split_BaseExceptionGroup_subclass_no_derive_new_override(self): + class EG(BaseExceptionGroup): + def __new__(cls, message, excs, unused): + # The "unused" arg is here to show that split() doesn't call + # the actual class constructor from the default derive() + # implementation (it would fail on unused arg if so because + # it assumes the BaseExceptionGroup.__new__ signature). + return super().__new__(cls, message, excs) + + try: + raise EG("eg", [ValueError(1), KeyboardInterrupt(2)], "unused") + except EG as e: + eg = e + + self.assertMatchesTemplate(eg, EG, [ValueError(1), KeyboardInterrupt(2)]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate( + rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)] + ) + + # Match Everything + match, rest = self.split_exception_group(eg, (ValueError, KeyboardInterrupt)) + self.assertMatchesTemplate( + match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)] + ) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)]) + self.assertMatchesTemplate(rest, BaseExceptionGroup, [KeyboardInterrupt(2)]) + + # Match KeyboardInterrupt + match, rest = self.split_exception_group(eg, KeyboardInterrupt) + self.assertMatchesTemplate(match, BaseExceptionGroup, [KeyboardInterrupt(2)]) + self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)]) + + def test_split_ExceptionGroup_subclass_derive_and_new_overrides(self): + class EG(ExceptionGroup): + def __new__(cls, message, excs, code): + obj = super().__new__(cls, message, excs) + obj.code = code + return obj + + def derive(self, excs): + return EG(self.message, excs, self.code) + + try: + try: + try: + raise TypeError(2) + except TypeError as te: + raise EG("nested", [te], 101) + except EG as nested: + try: + raise ValueError(1) + except ValueError as ve: + raise EG("eg", [ve, nested], 42) + except EG as e: + e.add_note("hello") + eg = e + + self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]]) + + # Match Nothing + match, rest = self.split_exception_group(eg, OSError) + self.assertIsNone(match) + self.assertMatchesTemplate(rest, EG, [ValueError(1), [TypeError(2)]]) + self.assertEqual(rest.code, 42) + self.assertEqual(rest.exceptions[1].code, 101) + self.assertEqual(rest.__notes__, ["hello"]) + + # Match Everything + match, rest = self.split_exception_group(eg, (ValueError, TypeError)) + self.assertMatchesTemplate(match, EG, [ValueError(1), [TypeError(2)]]) + self.assertEqual(match.code, 42) + self.assertEqual(match.exceptions[1].code, 101) + self.assertEqual(match.__notes__, ["hello"]) + self.assertIsNone(rest) + + # Match ValueErrors + match, rest = self.split_exception_group(eg, ValueError) + self.assertMatchesTemplate(match, EG, [ValueError(1)]) + self.assertEqual(match.code, 42) + self.assertEqual(match.__notes__, ["hello"]) + self.assertMatchesTemplate(rest, EG, [[TypeError(2)]]) + self.assertEqual(rest.code, 42) + self.assertEqual(rest.exceptions[0].code, 101) + self.assertEqual(rest.__notes__, ["hello"]) + + # Match TypeErrors + match, rest = self.split_exception_group(eg, TypeError) + self.assertMatchesTemplate(match, EG, [[TypeError(2)]]) + self.assertEqual(match.code, 42) + self.assertEqual(match.exceptions[0].code, 101) + self.assertEqual(match.__notes__, ["hello"]) + self.assertMatchesTemplate(rest, EG, [ValueError(1)]) + self.assertEqual(rest.code, 42) + self.assertEqual(rest.__notes__, ["hello"]) + + +def test_repr(): + group = BaseExceptionGroup("foo", [ValueError(1), KeyboardInterrupt()]) + assert repr(group) == ( + "BaseExceptionGroup('foo', [ValueError(1), KeyboardInterrupt()])" + ) + + group = ExceptionGroup("foo", [ValueError(1), RuntimeError("bar")]) + assert repr(group) == "ExceptionGroup('foo', [ValueError(1), RuntimeError('bar')])" diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_formatting.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_formatting.py new file mode 100644 index 00000000000..f6b9bc2c455 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_formatting.py @@ -0,0 +1,538 @@ +import sys +import traceback +from typing import NoReturn +from urllib.error import HTTPError + +import pytest +from _pytest.capture import CaptureFixture +from _pytest.fixtures import SubRequest +from _pytest.monkeypatch import MonkeyPatch + +from exceptiongroup import ExceptionGroup + + +def raise_excgroup() -> NoReturn: + exceptions = [] + try: + raise ValueError("foo") + except ValueError as exc: + exceptions.append(exc) + + try: + raise RuntimeError("bar") + except RuntimeError as exc: + exc.__notes__ = ["Note from bar handler"] + exceptions.append(exc) + + exc = ExceptionGroup("test message", exceptions) + exc.add_note("Displays notes attached to the group too") + raise exc + + +@pytest.fixture( + params=[ + pytest.param(True, id="patched"), + pytest.param( + False, + id="unpatched", + marks=[ + pytest.mark.skipif( + sys.version_info >= (3, 11), + reason="No patching is done on Python >= 3.11", + ) + ], + ), + ], +) +def patched(request: SubRequest) -> bool: + return request.param + + +@pytest.fixture( + params=[pytest.param(False, id="newstyle"), pytest.param(True, id="oldstyle")] +) +def old_argstyle(request: SubRequest) -> bool: + return request.param + + +def test_exceptionhook(capsys: CaptureFixture) -> None: + try: + raise_excgroup() + except ExceptionGroup as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + + local_lineno = test_exceptionhook.__code__.co_firstlineno + lineno = raise_excgroup.__code__.co_firstlineno + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {local_lineno + 2}, in test_exceptionhook + | raise_excgroup() + | File "{__file__}", line {lineno + 15}, in raise_excgroup + | raise exc + | {module_prefix}ExceptionGroup: test message (2 sub-exceptions) + | Displays notes attached to the group too + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 3}, in raise_excgroup + | raise ValueError("foo") + | ValueError: foo + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 8}, in raise_excgroup + | raise RuntimeError("bar") + | RuntimeError: bar + | Note from bar handler + +------------------------------------ +""" + ) + + +def test_exceptiongroup_as_cause(capsys: CaptureFixture) -> None: + try: + raise Exception() from ExceptionGroup("", (Exception(),)) + except Exception as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + + lineno = test_exceptiongroup_as_cause.__code__.co_firstlineno + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + | {module_prefix}ExceptionGroup: (1 sub-exception) + +-+---------------- 1 ---------------- + | Exception + +------------------------------------ + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "{__file__}", line {lineno + 2}, in test_exceptiongroup_as_cause + raise Exception() from ExceptionGroup("", (Exception(),)) +Exception +""" + ) + + +def test_exceptiongroup_loop(capsys: CaptureFixture) -> None: + e0 = Exception("e0") + eg0 = ExceptionGroup("eg0", (e0,)) + eg1 = ExceptionGroup("eg1", (eg0,)) + + try: + raise eg0 from eg1 + except ExceptionGroup as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + + lineno = test_exceptiongroup_loop.__code__.co_firstlineno + 6 + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + | {module_prefix}ExceptionGroup: eg1 (1 sub-exception) + +-+---------------- 1 ---------------- + | Exception Group Traceback (most recent call last): + | File "{__file__}", line {lineno}, in test_exceptiongroup_loop + | raise eg0 from eg1 + | {module_prefix}ExceptionGroup: eg0 (1 sub-exception) + +-+---------------- 1 ---------------- + | Exception: e0 + +------------------------------------ + +The above exception was the direct cause of the following exception: + + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {lineno}, in test_exceptiongroup_loop + | raise eg0 from eg1 + | {module_prefix}ExceptionGroup: eg0 (1 sub-exception) + +-+---------------- 1 ---------------- + | Exception: e0 + +------------------------------------ +""" + ) + + +def test_exceptionhook_format_exception_only(capsys: CaptureFixture) -> None: + try: + raise_excgroup() + except ExceptionGroup as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + + local_lineno = test_exceptionhook_format_exception_only.__code__.co_firstlineno + lineno = raise_excgroup.__code__.co_firstlineno + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {local_lineno + 2}, in \ +test_exceptionhook_format_exception_only + | raise_excgroup() + | File "{__file__}", line {lineno + 15}, in raise_excgroup + | raise exc + | {module_prefix}ExceptionGroup: test message (2 sub-exceptions) + | Displays notes attached to the group too + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 3}, in raise_excgroup + | raise ValueError("foo") + | ValueError: foo + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 8}, in raise_excgroup + | raise RuntimeError("bar") + | RuntimeError: bar + | Note from bar handler + +------------------------------------ +""" + ) + + +def test_formatting_syntax_error(capsys: CaptureFixture) -> None: + try: + exec("//serser") + except SyntaxError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + + if sys.version_info >= (3, 10): + underline = "\n ^^" + elif sys.version_info >= (3, 8): + underline = "\n ^" + else: + underline = "\n ^" + + lineno = test_formatting_syntax_error.__code__.co_firstlineno + output = capsys.readouterr().err + assert output == ( + f"""\ +Traceback (most recent call last): + File "{__file__}", line {lineno + 2}, \ +in test_formatting_syntax_error + exec("//serser") + File "<string>", line 1 + //serser{underline} +SyntaxError: invalid syntax +""" + ) + + +def test_format_exception( + patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import format_exception + + exceptions = [] + try: + raise ValueError("foo") + except ValueError as exc: + exceptions.append(exc) + + try: + raise RuntimeError("bar") + except RuntimeError as exc: + exc.__notes__ = ["Note from bar handler"] + exceptions.append(exc) + + try: + raise_excgroup() + except ExceptionGroup as exc: + if old_argstyle: + lines = format_exception(type(exc), exc, exc.__traceback__) + else: + lines = format_exception(exc) + + local_lineno = test_format_exception.__code__.co_firstlineno + lineno = raise_excgroup.__code__.co_firstlineno + assert isinstance(lines, list) + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + assert "".join(lines) == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {local_lineno + 25}, in test_format_exception + | raise_excgroup() + | File "{__file__}", line {lineno + 15}, in raise_excgroup + | raise exc + | {module_prefix}ExceptionGroup: test message (2 sub-exceptions) + | Displays notes attached to the group too + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 3}, in raise_excgroup + | raise ValueError("foo") + | ValueError: foo + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 8}, in raise_excgroup + | raise RuntimeError("bar") + | RuntimeError: bar + | Note from bar handler + +------------------------------------ +""" + ) + + +def test_format_nested(monkeypatch: MonkeyPatch) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import format_exception + + def raise_exc(max_level: int, level: int = 1) -> NoReturn: + if level == max_level: + raise Exception(f"LEVEL_{level}") + else: + try: + raise_exc(max_level, level + 1) + except Exception: + raise Exception(f"LEVEL_{level}") + + try: + raise_exc(3) + except Exception as exc: + lines = format_exception(type(exc), exc, exc.__traceback__) + + local_lineno = test_format_nested.__code__.co_firstlineno + 20 + raise_exc_lineno1 = raise_exc.__code__.co_firstlineno + 2 + raise_exc_lineno2 = raise_exc.__code__.co_firstlineno + 5 + raise_exc_lineno3 = raise_exc.__code__.co_firstlineno + 7 + assert isinstance(lines, list) + assert "".join(lines) == ( + f"""\ +Traceback (most recent call last): + File "{__file__}", line {raise_exc_lineno2}, in raise_exc + raise_exc(max_level, level + 1) + File "{__file__}", line {raise_exc_lineno1}, in raise_exc + raise Exception(f"LEVEL_{{level}}") +Exception: LEVEL_3 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "{__file__}", line {raise_exc_lineno2}, in raise_exc + raise_exc(max_level, level + 1) + File "{__file__}", line {raise_exc_lineno3}, in raise_exc + raise Exception(f"LEVEL_{{level}}") +Exception: LEVEL_2 + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "{__file__}", line {local_lineno}, in test_format_nested + raise_exc(3) + File "{__file__}", line {raise_exc_lineno3}, in raise_exc + raise Exception(f"LEVEL_{{level}}") +Exception: LEVEL_1 +""" + ) + + +def test_format_exception_only( + patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import format_exception_only + + try: + raise_excgroup() + except ExceptionGroup as exc: + if old_argstyle: + output = format_exception_only(type(exc), exc) + else: + output = format_exception_only(exc) + + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + assert output == [ + f"{module_prefix}ExceptionGroup: test message (2 sub-exceptions)\n", + "Displays notes attached to the group too\n", + ] + + +def test_print_exception( + patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import print_exception + + try: + raise_excgroup() + except ExceptionGroup as exc: + if old_argstyle: + print_exception(type(exc), exc, exc.__traceback__) + else: + print_exception(exc) + + local_lineno = test_print_exception.__code__.co_firstlineno + lineno = raise_excgroup.__code__.co_firstlineno + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {local_lineno + 13}, in test_print_exception + | raise_excgroup() + | File "{__file__}", line {lineno + 15}, in raise_excgroup + | raise exc + | {module_prefix}ExceptionGroup: test message (2 sub-exceptions) + | Displays notes attached to the group too + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 3}, in raise_excgroup + | raise ValueError("foo") + | ValueError: foo + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 8}, in raise_excgroup + | raise RuntimeError("bar") + | RuntimeError: bar + | Note from bar handler + +------------------------------------ +""" + ) + + +def test_print_exc( + patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import print_exc + + try: + raise_excgroup() + except ExceptionGroup: + print_exc() + local_lineno = test_print_exc.__code__.co_firstlineno + lineno = raise_excgroup.__code__.co_firstlineno + module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup." + output = capsys.readouterr().err + assert output == ( + f"""\ + + Exception Group Traceback (most recent call last): + | File "{__file__}", line {local_lineno + 13}, in test_print_exc + | raise_excgroup() + | File "{__file__}", line {lineno + 15}, in raise_excgroup + | raise exc + | {module_prefix}ExceptionGroup: test message (2 sub-exceptions) + | Displays notes attached to the group too + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 3}, in raise_excgroup + | raise ValueError("foo") + | ValueError: foo + +---------------- 2 ---------------- + | Traceback (most recent call last): + | File "{__file__}", line {lineno + 8}, in raise_excgroup + | raise RuntimeError("bar") + | RuntimeError: bar + | Note from bar handler + +------------------------------------ +""" + ) + + +@pytest.mark.skipif( + not hasattr(NameError, "name") or sys.version_info[:2] == (3, 11), + reason="only works if NameError exposes the missing name", +) +def test_nameerror_suggestions( + patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import print_exc + + try: + folder + except NameError: + print_exc() + output = capsys.readouterr().err + assert "Did you mean" in output and "'filter'?" in output + + +@pytest.mark.skipif( + not hasattr(AttributeError, "name") or sys.version_info[:2] == (3, 11), + reason="only works if AttributeError exposes the missing name", +) +def test_nameerror_suggestions_in_group( + patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import print_exception + + try: + [].attend + except AttributeError as e: + eg = ExceptionGroup("a", [e]) + print_exception(eg) + output = capsys.readouterr().err + assert "Did you mean" in output and "'append'?" in output + + +def test_bug_suggestions_attributeerror_no_obj( + patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + if not patched: + # Block monkey patching, then force the module to be re-imported + del sys.modules["traceback"] + del sys.modules["exceptiongroup"] + del sys.modules["exceptiongroup._formatting"] + monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args)) + + from exceptiongroup import print_exception + + class NamedAttributeError(AttributeError): + def __init__(self, name: str) -> None: + self.name: str = name + + try: + raise NamedAttributeError(name="mykey") + except AttributeError as e: + print_exception(e) # does not crash + output = capsys.readouterr().err + assert "NamedAttributeError" in output + + +def test_works_around_httperror_bug(): + # See https://github.com/python/cpython/issues/98778 in Python <= 3.9 + err = HTTPError("url", 405, "METHOD NOT ALLOWED", None, None) + traceback.TracebackException(type(err), err, None) diff --git a/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_suppress.py b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_suppress.py new file mode 100644 index 00000000000..289bb33c0e6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/exceptiongroup/tests/test_suppress.py @@ -0,0 +1,16 @@ +import sys + +import pytest + +from exceptiongroup import suppress + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup, ExceptionGroup + + +def test_suppress_exception(): + with pytest.raises(ExceptionGroup) as exc, suppress(SystemExit): + raise BaseExceptionGroup("", [SystemExit(1), RuntimeError("boo")]) + + assert len(exc.value.exceptions) == 1 + assert isinstance(exc.value.exceptions[0], RuntimeError) diff --git a/tests/wpt/tests/tools/third_party/pluggy/.coveragerc b/tests/wpt/tests/tools/third_party/pluggy/.coveragerc index 1b1de1cd247..6801ce8b1f9 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/.coveragerc +++ b/tests/wpt/tests/tools/third_party/pluggy/.coveragerc @@ -12,3 +12,18 @@ source = pluggy/ */lib/python*/site-packages/pluggy/ */pypy*/site-packages/pluggy/ *\Lib\site-packages\pluggy\ + +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + if TYPE_CHECKING: + if False: + + if __name__ == .__main__.: + + raise NotImplementedError + + # Ignore coverage on lines solely with `...` + ^\s*\.\.\.\s*$ diff --git a/tests/wpt/tests/tools/third_party/pluggy/.github/FUNDING.yml b/tests/wpt/tests/tools/third_party/pluggy/.github/FUNDING.yml new file mode 100644 index 00000000000..b4d3edff1f6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# info: +# * https://help.github.com/en/articles/displaying-a-sponsor-button-in-your-repository +# * https://tidelift.com/subscription/how-to-connect-tidelift-with-github +tidelift: pypi/pluggy +open_collective: pytest diff --git a/tests/wpt/tests/tools/third_party/pluggy/.github/workflows/main.yml b/tests/wpt/tests/tools/third_party/pluggy/.github/workflows/main.yml index e1022ca96dd..d8c0b181a7c 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/.github/workflows/main.yml +++ b/tests/wpt/tests/tools/third_party/pluggy/.github/workflows/main.yml @@ -19,62 +19,64 @@ jobs: fail-fast: false matrix: name: [ - "windows-py36", - "windows-py39", + "windows-py38", + "windows-py311", "windows-pypy3", - "ubuntu-py36", - "ubuntu-py36-pytestmain", - "ubuntu-py37", + "ubuntu-py38-pytestmain", "ubuntu-py38", "ubuntu-py39", + "ubuntu-py310", + "ubuntu-py311", + "ubuntu-py312", "ubuntu-pypy3", "ubuntu-benchmark", - - "linting", - "docs", ] include: - - name: "windows-py36" - python: "3.6" + - name: "windows-py38" + python: "3.8" os: windows-latest - tox_env: "py36" - - name: "windows-py39" - python: "3.9" + tox_env: "py38" + - name: "windows-py311" + python: "3.10" os: windows-latest - tox_env: "py39" + tox_env: "py311" - name: "windows-pypy3" - python: "pypy3" + python: "pypy3.9" os: windows-latest tox_env: "pypy3" - - name: "ubuntu-py36" - python: "3.6" - os: ubuntu-latest - tox_env: "py36" - use_coverage: true - - name: "ubuntu-py36-pytestmain" - python: "3.6" - os: ubuntu-latest - tox_env: "py36-pytestmain" - use_coverage: true - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - tox_env: "py37" - use_coverage: true - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest tox_env: "py38" use_coverage: true + - name: "ubuntu-py38-pytestmain" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-pytestmain" + use_coverage: true - name: "ubuntu-py39" python: "3.9" os: ubuntu-latest tox_env: "py39" use_coverage: true + - name: "ubuntu-py310" + python: "3.10" + os: ubuntu-latest + tox_env: "py310" + - name: "ubuntu-py311" + python: "3.11" + os: ubuntu-latest + tox_env: "py311" + use_coverage: true + - name: "ubuntu-py312" + python: "3.12-dev" + os: ubuntu-latest + tox_env: "py312" + use_coverage: true - name: "ubuntu-pypy3" - python: "pypy3" + python: "pypy3.9" os: ubuntu-latest tox_env: "pypy3" use_coverage: true @@ -82,22 +84,14 @@ jobs: python: "3.8" os: ubuntu-latest tox_env: "benchmark" - - name: "linting" - python: "3.8" - os: ubuntu-latest - tox_env: "linting" - - name: "docs" - python: "3.8" - os: ubuntu-latest - tox_env: "docs" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -116,33 +110,36 @@ jobs: - name: Upload coverage if: matrix.use_coverage && github.repository == 'pytest-dev/pluggy' - env: - CODECOV_NAME: ${{ matrix.name }} - run: bash scripts/upload-coverage.sh -F GHA,${{ runner.os }} + uses: codecov/codecov-action@v4 + continue-on-error: true + with: + fail_ci_if_error: true + files: ./coverage.xml + verbose: true deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pluggy' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install --upgrade wheel setuptools setuptools_scm + pip install --upgrade build - name: Build package - run: python setup.py sdist bdist_wheel + run: python -m build --sdist --wheel --outdir dist/ - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.4.1 + uses: pypa/gh-action-pypi-publish@v1.8.6 with: user: __token__ password: ${{ secrets.pypi_token }} diff --git a/tests/wpt/tests/tools/third_party/pluggy/.gitignore b/tests/wpt/tests/tools/third_party/pluggy/.gitignore index 4580536c7ad..1a33c625bbe 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/.gitignore +++ b/tests/wpt/tests/tools/third_party/pluggy/.gitignore @@ -39,6 +39,7 @@ htmlcov/ .coverage .coverage.* .cache +.mypy_cache/ nosetests.xml coverage.xml *,cover @@ -52,6 +53,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/_changelog_towncrier_draft.rst # PyBuilder target/ @@ -62,3 +64,6 @@ src/pluggy/_version.py # generated by pip pip-wheel-metadata/ + +# pytest-benchmark +.benchmarks/ diff --git a/tests/wpt/tests/tools/third_party/pluggy/.pre-commit-config.yaml b/tests/wpt/tests/tools/third_party/pluggy/.pre-commit-config.yaml index d919ffeb2f6..0532a6f7bf6 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/.pre-commit-config.yaml +++ b/tests/wpt/tests/tools/third_party/pluggy/.pre-commit-config.yaml @@ -1,34 +1,51 @@ repos: -- repo: https://github.com/ambv/black - rev: 21.7b0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.3.5" + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format +- repo: https://github.com/PyCQA/autoflake + rev: v2.3.1 hooks: - - id: black - args: [--safe, --quiet] -- repo: https://github.com/asottile/blacken-docs - rev: v1.10.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==21.7b0] + - id: autoflake + name: autoflake + args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"] + language: python + files: \.py$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: flake8 + additional_dependencies: [flake8-typing-imports] +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.2 + hooks: + - id: pyupgrade + args: [--py38-plus] +- repo: https://github.com/asottile/blacken-docs + rev: 1.16.0 + hooks: + - id: blacken-docs + additional_dependencies: [black==24.2.0] - repo: local hooks: - id: rst name: rst entry: rst-lint --encoding utf-8 - files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$ + files: ^(HOWTORELEASE.rst|README.rst)$ language: python additional_dependencies: [pygments, restructuredtext_lint] -- repo: https://github.com/pre-commit/pygrep-hooks +- repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 hooks: - - id: rst-backticks -- repo: https://github.com/asottile/pyupgrade - rev: v2.23.3 - hooks: - - id: pyupgrade - args: [--py36-plus] + - id: mypy + files: ^(src/|testing/) + args: [] + additional_dependencies: [pytest] diff --git a/tests/wpt/tests/tools/third_party/pluggy/.readthedocs.yml b/tests/wpt/tests/tools/third_party/pluggy/.readthedocs.yml new file mode 100644 index 00000000000..d751431ea38 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 + +python: + install: + # Without this, sphinx can't find pluggy's version. + - method: pip + path: . + - requirements: docs/requirements.txt + +build: + os: ubuntu-22.04 + tools: + python: "3.11" diff --git a/tests/wpt/tests/tools/third_party/pluggy/CHANGELOG.rst b/tests/wpt/tests/tools/third_party/pluggy/CHANGELOG.rst index 13a388c435a..a597dc370a6 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/CHANGELOG.rst +++ b/tests/wpt/tests/tools/third_party/pluggy/CHANGELOG.rst @@ -2,8 +2,145 @@ Changelog ========= +Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``). + +.. + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/contributing/#news-entries + we named the news folder changelog + +.. only:: changelog_towncrier_draft + + .. The 'changelog_towncrier_draft' tag is included by our 'tox -e docs', + but not on readthedocs. + + .. include:: _changelog_towncrier_draft.rst + .. towncrier release notes start +pluggy 1.5.0 (2024-04-19) +========================= + +Features +-------- + +- `#178 <https://github.com/pytest-dev/pluggy/issues/178>`_: Add support for deprecating specific hook parameters, or more generally, for issuing a warning whenever a hook implementation requests certain parameters. + + See :ref:`warn_on_impl` for details. + + + +Bug Fixes +--------- + +- `#481 <https://github.com/pytest-dev/pluggy/issues/481>`_: ``PluginManager.get_plugins()`` no longer returns ``None`` for blocked plugins. + + +pluggy 1.4.0 (2024-01-24) +========================= + +Features +-------- + +- `#463 <https://github.com/pytest-dev/pluggy/issues/463>`_: A warning :class:`~pluggy.PluggyTeardownRaisedWarning` is now issued when an old-style hookwrapper raises an exception during teardown. + See the warning documentation for more details. + +- `#471 <https://github.com/pytest-dev/pluggy/issues/471>`_: Add :func:`PluginManager.unblock <pluggy.PluginManager.unblock>` method to unblock a plugin by plugin name. + +Bug Fixes +--------- + +- `#441 <https://github.com/pytest-dev/pluggy/issues/441>`_: Fix :func:`~pluggy.HookCaller.call_extra()` extra methods getting ordered before everything else in some circumstances. Regressed in pluggy 1.1.0. + +- `#438 <https://github.com/pytest-dev/pluggy/issues/438>`_: Fix plugins registering other plugins in a hook when the other plugins implement the same hook itself. Regressed in pluggy 1.1.0. + + +pluggy 1.3.0 (2023-08-26) +========================= + +Deprecations and Removals +------------------------- + +- `#426 <https://github.com/pytest-dev/pluggy/issues/426>`_: Python 3.7 is no longer supported. + + + +Features +-------- + +- `#428 <https://github.com/pytest-dev/pluggy/issues/428>`_: Pluggy now exposes its typings to static type checkers. + + As part of this, the following changes are made: + + - Renamed ``_Result`` to ``Result``, and exported as :class:`pluggy.Result`. + - Renamed ``_HookRelay`` to ``HookRelay``, and exported as :class:`pluggy.HookRelay`. + - Renamed ``_HookCaller`` to ``HookCaller``, and exported as :class:`pluggy.HookCaller`. + - Exported ``HookImpl`` as :class:`pluggy.HookImpl`. + - Renamed ``_HookImplOpts`` to ``HookimplOpts``, and exported as :class:`pluggy.HookimplOpts`. + - Renamed ``_HookSpecOpts`` to ``HookspecOpts``, and exported as :class:`pluggy.HookspecOpts`. + - Some fields and classes are marked ``Final`` and ``@final``. + - The :ref:`api-reference` is updated to clearly delineate pluggy's public API. + + Compatibility aliases are put in place for the renamed types. + We do not plan to remove the aliases, but we strongly recommend to only import from ``pluggy.*`` to ensure future compatibility. + + Please note that pluggy is currently unable to provide strong typing for hook calls, e.g. ``pm.hook.my_hook(...)``, + nor to statically check that a hook implementation matches the hook specification's type. + + +pluggy 1.2.0 (2023-06-21) +========================= + +Features +-------- + +- `#405 <https://github.com/pytest-dev/pluggy/issues/405>`_: The new-style hook wrappers, added in the yanked 1.1.0 release, now require an explicit ``wrapper=True`` designation in the ``@hookimpl()`` decorator. + + +pluggy 1.1.0 (YANKED) +===================== + +.. note:: + + This release was yanked because unfortunately the implicit new-style hook wrappers broke some downstream projects. + See `#403 <https://github.com/pytest-dev/pluggy/issues/403>`__ for more information. + This was rectified in the 1.2.0 release. + +Deprecations and Removals +------------------------- + +- `#364 <https://github.com/pytest-dev/pluggy/issues/364>`_: Python 3.6 is no longer supported. + + + +Features +-------- + +- `#260 <https://github.com/pytest-dev/pluggy/issues/260>`_: Added "new-style" hook wrappers, a simpler but equally powerful alternative to the existing ``hookwrapper=True`` wrappers. + + New-style wrappers are generator functions, similarly to ``hookwrapper``, but do away with the :class:`result <pluggy.Result>` object. + Instead, the return value is sent directly to the ``yield`` statement, or, if inner calls raised an exception, it is raised from the ``yield``. + The wrapper is expected to return a value or raise an exception, which will become the result of the hook call. + + New-style wrappers are fully interoperable with old-style wrappers. + We encourage users to use the new style, however we do not intend to deprecate the old style any time soon. + + See :ref:`hookwrappers` for the full documentation. + + +- `#364 <https://github.com/pytest-dev/pluggy/issues/364>`_: Python 3.11 and 3.12 are now officially supported. + + +- `#394 <https://github.com/pytest-dev/pluggy/issues/394>`_: Added the :meth:`~pluggy.Result.force_exception` method to ``_Result``. + + ``force_exception`` allows (old-style) hookwrappers to force an exception or override/adjust an existing exception of a hook invocation, + in a properly behaving manner. Using ``force_exception`` is preferred over raising an exception from the hookwrapper, + because raising an exception causes other hookwrappers to be skipped. + + pluggy 1.0.0 (2021-08-25) ========================= @@ -48,7 +185,7 @@ Features -------- - `#282 <https://github.com/pytest-dev/pluggy/issues/282>`_: When registering a hookimpl which is declared as ``hookwrapper=True`` but whose - function is not a generator function, a ``PluggyValidationError`` exception is + function is not a generator function, a :class:`~pluggy.PluginValidationError` exception is now raised. Previously this problem would cause an error only later, when calling the hook. @@ -58,7 +195,7 @@ Features .. code-block:: python - def my_hook_real_implementation(arg): + def my_hook_implementation(arg): print("before") yield print("after") @@ -79,6 +216,10 @@ Features - `#309 <https://github.com/pytest-dev/pluggy/issues/309>`_: Add official support for Python 3.9. +- `#251 <https://github.com/pytest-dev/pluggy/issues/251>`_: Add ``specname`` option to ``@hookimpl``. If ``specname`` is provided, it will be used + instead of the function name when matching this hook implementation to a hook specification during registration (allowing a plugin to register + a hook implementation that was not named the same thing as the corresponding ``@hookspec``). + pluggy 0.13.1 (2019-11-21) ========================== diff --git a/tests/wpt/tests/tools/third_party/pluggy/README.rst b/tests/wpt/tests/tools/third_party/pluggy/README.rst index 3496617e1e8..7a58c1b9c8d 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/README.rst +++ b/tests/wpt/tests/tools/third_party/pluggy/README.rst @@ -99,3 +99,18 @@ Running this directly gets us:: http://doc.devpi.net .. _read the docs: https://pluggy.readthedocs.io/en/latest/ + + +Support pluggy +-------------- + +`Open Collective`_ is an online funding platform for open and transparent communities. +It provides tools to raise money and share your finances in full transparency. + +It is the platform of choice for individuals and companies that want to make one-time or +monthly donations directly to the project. + +``pluggy`` is part of the ``pytest-dev`` project, see more details in the `pytest collective`_. + +.. _Open Collective: https://opencollective.com +.. _pytest collective: https://opencollective.com/pytest diff --git a/tests/wpt/tests/tools/third_party/pluggy/RELEASING.rst b/tests/wpt/tests/tools/third_party/pluggy/RELEASING.rst index ee0d1331e0a..3d6ba16c16b 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/RELEASING.rst +++ b/tests/wpt/tests/tools/third_party/pluggy/RELEASING.rst @@ -1,6 +1,10 @@ Release Procedure ----------------- +#. Dependening on the magnitude of the changes in the release, consider testing + some of the large downstream users of pluggy against the upcoming release. + You can do so using the scripts in the ``downstream/`` directory. + #. From a clean work tree, execute:: tox -e release -- VERSION diff --git a/tests/wpt/tests/tools/third_party/pluggy/SECURITY.md b/tests/wpt/tests/tools/third_party/pluggy/SECURITY.md new file mode 100644 index 00000000000..6d3c7348e89 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/SECURITY.md @@ -0,0 +1,3 @@ +# Security + +Please report any security vulnerabilities at https://github.com/pytest-dev/pluggy/security. diff --git a/tests/wpt/tests/tools/third_party/pluggy/TIDELIFT.rst b/tests/wpt/tests/tools/third_party/pluggy/TIDELIFT.rst new file mode 100644 index 00000000000..93af996b296 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/TIDELIFT.rst @@ -0,0 +1,60 @@ +======== +Tidelift +======== + +pluggy is a member of `Tidelift`_. This document describes how the core team manages +Tidelift-related activities. + +What is it +========== + +Tidelift aims to make Open Source sustainable by offering subscriptions to companies which rely +on Open Source packages. This subscription allows it to pay maintainers of those Open Source +packages to aid sustainability of the work. + +It is the perfect platform for companies that want to support Open Source packages and at the same +time obtain assurances regarding maintenance, quality and security. + +Funds +===== + +It was decided in the `mailing list`_ that the Tidelift contribution will be split evenly between +members of the `contributors team`_ interested in receiving funding. + +The current list of contributors receiving funding are: + +* `@nicoddemus`_ +* `@The-Compiler`_ +* `@RonnyPfannschmidt`_ + +Contributors interested in receiving a part of the funds just need to submit a PR adding their +name to the list. Contributors that want to stop receiving the funds should also submit a PR +in the same way. + +The PR should mention `@pytest-dev/tidelift-admins`_ so appropriate changes +can be made in the Tidelift platform. + +After the PR has been accepted and merged, the contributor should register in the `Tidelift`_ +platform and follow the instructions there, including signing an `agreement`_. + +Admins +====== + +A few people have admin access to the Tidelift dashboard to make changes. Those people +are part of the `@pytest-dev/tidelift-admins`_ team. + +`Core contributors`_ interested in helping out with Tidelift maintenance are welcome! We don't +expect much work here other than the occasional adding/removal of a contributor from receiving +funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the mailing list. + + +.. _`Tidelift`: https://tidelift.com +.. _`mailing list`: https://mail.python.org/pipermail/pytest-dev/2019-May/004716.html +.. _`contributors team`: https://github.com/orgs/pytest-dev/teams/contributors +.. _`core contributors`: https://github.com/orgs/pytest-dev/teams/core/members +.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members +.. _`agreement`: https://tidelift.com/docs/lifting/agreement + +.. _`@nicoddemus`: https://github.com/nicoddemus +.. _`@The-Compiler`: https://github.com/The-Compiler +.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/api_reference.rst b/tests/wpt/tests/tools/third_party/pluggy/docs/api_reference.rst index d9552d44858..b14d725d94c 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/api_reference.rst +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/api_reference.rst @@ -1,19 +1,61 @@ :orphan: -Api Reference +.. _`api-reference`: + +API Reference ============= -.. automodule:: pluggy +.. autoclass:: pluggy.PluginManager + :members: + +.. autoclass:: pluggy.PluginValidationError + :show-inheritance: + :members: + +.. autodecorator:: pluggy.HookspecMarker + +.. autodecorator:: pluggy.HookimplMarker + +.. autoclass:: pluggy.HookRelay() + :members: + + .. data:: <hook name> + + :type: HookCaller + + The caller for the hook with the given name. + +.. autoclass:: pluggy.HookCaller() + :members: + :special-members: __call__ + +.. autoclass:: pluggy.HookCallError() + :show-inheritance: :members: - :undoc-members: + +.. autoclass:: pluggy.Result() :show-inheritance: + :members: -.. autoclass:: pluggy._callers._Result -.. automethod:: pluggy._callers._Result.get_result -.. automethod:: pluggy._callers._Result.force_result +.. autoclass:: pluggy.HookImpl() + :members: -.. autoclass:: pluggy._hooks._HookCaller -.. automethod:: pluggy._hooks._HookCaller.call_extra -.. automethod:: pluggy._hooks._HookCaller.call_historic +.. autoclass:: pluggy.HookspecOpts() + :show-inheritance: + :members: -.. autoclass:: pluggy._hooks._HookRelay +.. autoclass:: pluggy.HookimplOpts() + :show-inheritance: + :members: + + +Warnings +-------- + +Custom warnings generated in some situations such as improper usage or deprecated features. + +.. autoclass:: pluggy.PluggyWarning() + :show-inheritance: + +.. autoclass:: pluggy.PluggyTeardownRaisedWarning() + :show-inheritance: diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/conf.py b/tests/wpt/tests/tools/third_party/pluggy/docs/conf.py index f8e70c88bf3..e5151c5a45c 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/conf.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/conf.py @@ -1,9 +1,9 @@ -import sys +from importlib import metadata +from typing import TYPE_CHECKING -if sys.version_info >= (3, 8): - from importlib import metadata -else: - import importlib_metadata as metadata + +if TYPE_CHECKING: + import sphinx.application extensions = [ @@ -33,7 +33,7 @@ release = metadata.version(project) version = ".".join(release.split(".")[:2]) -language = None +language = "en" pygments_style = "sphinx" # html_logo = "_static/img/plug.png" @@ -46,8 +46,9 @@ html_theme_options = { "github_button": "true", "github_banner": "true", "github_type": "star", - "badge_branch": "master", + "badge_branch": "main", "page_width": "1080px", + "sidebar_width": "300px", "fixed_sidebar": "false", } html_sidebars = { @@ -59,6 +60,20 @@ html_static_path = ["_static"] # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "pluggy", "pluggy Documentation", [author], 1)] +autodoc_member_order = "bysource" + +nitpicky = True +nitpick_ignore = { + # Don't want to expose this yet (see #428). + ("py:class", "pluggy._tracing.TagTracerSub"), + # Compat hack, don't want to expose it. + ("py:class", "pluggy._manager.DistFacade"), + # `types.ModuleType` turns into `module` but then fails to resolve... + ("py:class", "module"), + # Just a TypeVar. + ("py:obj", "pluggy._result.ResultType"), + ("py:class", "pluggy._result.ResultType"), +} # -- Options for Texinfo output ------------------------------------------- @@ -81,7 +96,36 @@ texinfo_documents = [ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pytest": ("https://docs.pytest.org/en/latest", None), - "setuptools": ("https://setuptools.readthedocs.io/en/latest", None), - "tox": ("https://tox.readthedocs.io/en/latest", None), + "setuptools": ("https://setuptools.pypa.io/en/latest", None), + "tox": ("https://tox.wiki/en/latest", None), "devpi": ("https://devpi.net/docs/devpi/devpi/stable/+doc/", None), + "kedro": ("https://docs.kedro.org/en/latest/", None), } + + +def configure_logging(app: "sphinx.application.Sphinx") -> None: + """Configure Sphinx's WarningHandler to handle (expected) missing include.""" + import logging + + import sphinx.util.logging + + class WarnLogFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + """Ignore warnings about missing include with "only" directive. + + Ref: https://github.com/sphinx-doc/sphinx/issues/2150.""" + if ( + record.msg.startswith('Problems with "include" directive path:') + and "_changelog_towncrier_draft.rst" in record.msg + ): + return False + return True + + logger = logging.getLogger(sphinx.util.logging.NAMESPACE) + warn_handler = [x for x in logger.handlers if x.level == logging.WARNING] + assert len(warn_handler) == 1, warn_handler + warn_handler[0].filters.insert(0, WarnLogFilter()) + + +def setup(app: "sphinx.application.Sphinx") -> None: + configure_logging(app) diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py index f81a8eb4038..557aa5c1f13 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py @@ -1,5 +1,6 @@ from setuptools import setup + setup( name="eggsample-spam", install_requires="eggsample", diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py index 4dc4b36dec3..b2d9d8301fa 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py @@ -1,4 +1,5 @@ import pluggy + hookimpl = pluggy.HookimplMarker("eggsample") """Marker to be imported and used in plugins (and for own implementations)""" diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py index 48866b24912..4bab42281de 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py @@ -1,5 +1,6 @@ import pluggy + hookspec = pluggy.HookspecMarker("eggsample") diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py index ac1d33b4539..d1827879236 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py @@ -1,9 +1,11 @@ import itertools import random +from eggsample import hookspecs +from eggsample import lib + import pluggy -from eggsample import hookspecs, lib condiments_tray = {"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2} diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py index 8b3facb3b67..89e88ceca07 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py @@ -1,4 +1,6 @@ -from setuptools import setup, find_packages +from setuptools import find_packages +from setuptools import setup + setup( name="eggsample", diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/toy-example.py b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/toy-example.py index 6d2086f9ba3..c7d361bc4db 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/examples/toy-example.py +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/examples/toy-example.py @@ -1,5 +1,6 @@ import pluggy + hookspec = pluggy.HookspecMarker("myproject") hookimpl = pluggy.HookimplMarker("myproject") diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/index.rst b/tests/wpt/tests/tools/third_party/pluggy/docs/index.rst index eab08fcbbd6..b98c4956ba2 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/docs/index.rst +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/index.rst @@ -6,7 +6,7 @@ What is it? *********** ``pluggy`` is the crystallized core of :ref:`plugin management and hook calling <pytest:writing-plugins>` for :std:doc:`pytest <pytest:index>`. -It enables `500+ plugins`_ to extend and customize ``pytest``'s default +It enables `1400+ plugins`_ to extend and customize ``pytest``'s default behaviour. Even ``pytest`` itself is composed as a set of ``pluggy`` plugins which are invoked in sequence according to a well defined set of protocols. @@ -162,6 +162,7 @@ documentation and source code: * :ref:`pytest <pytest:writing-plugins>` * :std:doc:`tox <tox:plugins>` * :std:doc:`devpi <devpi:devguide/index>` +* :std:doc:`kedro <kedro:hooks/introduction>` For more details and advanced usage please read on. @@ -224,6 +225,8 @@ which has been appropriately marked. *hookimpls* are loaded from a plugin using the :py:meth:`~pluggy.PluginManager.register()` method: +*hookimpls* must be hashable. + .. code-block:: python import sys @@ -281,19 +284,22 @@ example above). Note: there is *no* strict requirement that each *hookimpl* has a corresponding *hookspec* (see :ref:`enforcing spec validation <enforcing>`). -*new in version 0.13.2:* +*new in version 1.0.0:* To override the default behavior, a *hookimpl* may also be matched to a *hookspec* in the *host* program with a non-matching function name by using the ``specname`` option. Continuing the example above, the *hookimpl* function does not need to be named ``setup_project``, but if the argument ``specname="setup_project"`` is provided to the ``hookimpl`` decorator, it will -be matched and checked against the ``setup_project`` hookspec: +be matched and checked against the ``setup_project`` hookspec. + +This is useful for registering multiple implementations of the same plugin in +the same Python file, for example: .. code-block:: python @hookimpl(specname="setup_project") - def any_plugin_function(config, args): + def setup_1(config, args): """This hook is used to process the initial config and possibly input arguments. """ @@ -302,6 +308,16 @@ be matched and checked against the ``setup_project`` hookspec: return config + + @hookimpl(specname="setup_project") + def setup_2(config, args): + """Perform additional setup steps""" + # ... + return config + + +.. _callorder: + Call time order ^^^^^^^^^^^^^^^ By default hooks are :ref:`called <calling>` in LIFO registered order, however, @@ -359,32 +375,108 @@ For another example see the :ref:`pytest:plugin-hookorder` section of the Wrappers ^^^^^^^^ -A *hookimpl* can be marked with a ``"hookwrapper"`` option which indicates that -the function will be called to *wrap* (or surround) all other normal *hookimpl* -calls. A *hookwrapper* can thus execute some code ahead and after the execution -of all corresponding non-wrappper *hookimpls*. -Much in the same way as a :py:func:`@contextlib.contextmanager <python:contextlib.contextmanager>`, *hookwrappers* must -be implemented as generator function with a single ``yield`` in its body: +.. note:: + This section describes "new-style hook wrappers", which were added in Pluggy + 1.1. For earlier versions, see the "old-style hook wrappers" section below. + + New-style hooks wrappers are declared with ``wrapper=True``, while + old-style hook wrappers are declared with ``hookwrapper=True``. + + The two styles are fully interoperable between plugins using different + styles. However within the same plugin we recommend using only one style, + for consistency. + +A *hookimpl* can be marked with the ``"wrapper"`` option, which indicates +that the function will be called to *wrap* (or surround) all other normal +*hookimpl* calls. A *hook wrapper* can thus execute some code ahead and after the +execution of all corresponding non-wrapper *hookimpls*. +Much in the same way as a :py:func:`@contextlib.contextmanager <python:contextlib.contextmanager>`, +*hook wrappers* must be implemented as generator function with a single ``yield`` in its body: .. code-block:: python - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def setup_project(config, args): """Wrap calls to ``setup_project()`` implementations which should return json encoded config options. """ + # get initial default config + defaults = config.tojson() + if config.debug: print("Pre-hook config is {}".format(config.tojson())) + # all corresponding hookimpls are invoked here + result = yield + + for item in result: + print("JSON config override is {}".format(item)) + + if config.debug: + print("Post-hook config is {}".format(config.tojson())) + + if config.use_defaults: + return defaults + else: + return result + +The generator is :py:meth:`sent <python:generator.send>` the return value +of the hook thus far, or, if the previous calls raised an exception, it is +:py:meth:`thrown <python:generator.throw>` the exception. + +The function should do one of two things: + +- Return a value, which can be the same value as received from the ``yield``, or something else entirely. + +- Raise an exception. + +The return value or exception propagate to further hook wrappers, and finally +to the hook caller. + +Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs. + +.. _old_style_hookwrappers: + +Old-style wrappers +^^^^^^^^^^^^^^^^^^ + +.. note:: + Prefer to use new-style hook wrappers, unless you need to support Pluggy + versions before 1.1. + +A *hookimpl* can be marked with the ``"hookwrapper"`` option, which indicates +that the function will be called to *wrap* (or surround) all other normal +*hookimpl* calls. A *hookwrapper* can thus execute some code ahead and after the +execution of all corresponding non-wrapper *hookimpls*. + +*hookwrappers* must be implemented as generator function with a single ``yield`` in its body: + + +.. code-block:: python + + @hookimpl(hookwrapper=True) + def setup_project(config, args): + """Wrap calls to ``setup_project()`` implementations which + should return json encoded config options. + """ # get initial default config defaults = config.tojson() + if config.debug: + print("Pre-hook config is {}".format(config.tojson())) + # all corresponding hookimpls are invoked here outcome = yield - for item in outcome.get_result(): + try: + result = outcome.get_result() + except BaseException as e: + outcome.force_exception(e) + return + + for item in result: print("JSON config override is {}".format(item)) if config.debug: @@ -393,17 +485,22 @@ be implemented as generator function with a single ``yield`` in its body: if config.use_defaults: outcome.force_result(defaults) -The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy._callers._Result` object which can -be assigned in the ``yield`` expression and used to override or inspect -the final result(s) returned back to the caller using the -:py:meth:`~pluggy._callers._Result.force_result` or -:py:meth:`~pluggy._callers._Result.get_result` methods. +The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy.Result` object which can +be assigned in the ``yield`` expression and used to inspect +the final result(s) or exceptions returned back to the caller using the +:py:meth:`~pluggy.Result.get_result` method, override the result +using the :py:meth:`~pluggy.Result.force_result`, or override +the exception using the :py:meth:`~pluggy.Result.force_exception` +method. .. note:: - Hook wrappers can **not** return results (as per generator function - semantics); they can only modify them using the ``_Result`` API. + Old-style hook wrappers can **not** return results; they can only modify + them using the :py:meth:`~pluggy.Result.force_result` API. -Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs. + Old-style Hook wrappers should **not** raise exceptions; this will cause + further hookwrappers to be skipped. They should use + :py:meth:`~pluggy.Result.force_exception` to adjust the + exception. .. _specs: @@ -469,7 +566,7 @@ then make sure to mark those hooks as :ref:`optional <optionalhook>`. Opt-in arguments ^^^^^^^^^^^^^^^^ To allow for *hookspecs* to evolve over the lifetime of a project, -*hookimpls* can accept **less** arguments then defined in the spec. +*hookimpls* can accept **less** arguments than defined in the spec. This allows for extending hook arguments (and thus semantics) without breaking existing *hookimpls*. @@ -511,7 +608,7 @@ First result only ^^^^^^^^^^^^^^^^^ A *hookspec* can be marked such that when the *hook* is called the call loop will only invoke up to the first *hookimpl* which returns a result other -then ``None``. +than ``None``. .. code-block:: python @@ -522,7 +619,7 @@ then ``None``. This can be useful for optimizing a call loop for which you are only interested in a single core *hookimpl*. An example is the :func:`~_pytest.hookspec.pytest_cmdline_main` central routine of ``pytest``. -Note that all ``hookwrappers`` are still invoked with the first result. +Note that all hook wrappers are still invoked with the first result. Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs. @@ -531,7 +628,7 @@ Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs. Historic hooks ^^^^^^^^^^^^^^ You can mark a *hookspec* as being *historic* meaning that the hook -can be called with :py:meth:`~pluggy._hooks._HookCaller.call_historic()` **before** +can be called with :py:meth:`~pluggy.HookCaller.call_historic()` **before** having been registered: .. code-block:: python @@ -549,22 +646,47 @@ dynamically loaded plugins. For more info see :ref:`call_historic`. +.. _warn_on_impl: + Warnings on hook implementation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ As projects evolve new hooks may be introduced and/or deprecated. -if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook. +If a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook. + + +.. code-block:: python + + @hookspec( + warn_on_impl=DeprecationWarning("old_hook is deprecated and will be removed soon") + ) + def old_hook(): + pass + +If you don't want to deprecate implementing the entire hook, but just specific +parameters of it, you can specify ``warn_on_impl_args``, a dict mapping +parameter names to warnings. The warnings will trigger whenever any plugin +implements the hook requesting one of the specified parameters. .. code-block:: python @hookspec( - warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon") + warn_on_impl_args={ + "lousy_arg": DeprecationWarning( + "The lousy_arg parameter of refreshed_hook is deprecated and will be removed soon; " + "use awesome_arg instead" + ), + }, ) - def oldhook(): + def refreshed_hook(lousy_arg, awesome_arg): pass +.. versionadded:: 1.5 + The ``warn_on_impl_args`` parameter. + + .. _manage: The Plugin registry @@ -653,13 +775,13 @@ The core functionality of ``pluggy`` enables an extension provider to override function calls made at certain points throughout a program. A particular *hook* is invoked by calling an instance of -a :py:class:`pluggy._hooks._HookCaller` which in turn *loops* through the +a :py:class:`pluggy.HookCaller` which in turn *loops* through the ``1:N`` registered *hookimpls* and calls them in sequence. Every :py:class:`~pluggy.PluginManager` has a ``hook`` attribute -which is an instance of this :py:class:`pluggy._hooks._HookRelay`. -The :py:class:`~pluggy._hooks._HookRelay` itself contains references -(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy._hooks._HookCaller` instance. +which is an instance of :py:class:`pluggy.HookRelay`. +The :py:class:`~pluggy.HookRelay` itself contains references +(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy.HookCaller` instance. More practically you call a *hook* like so: @@ -675,7 +797,7 @@ More practically you call a *hook* like so: pm.add_hookspecs(mypluginspec) pm.register(myplugin) - # we invoke the _HookCaller and thus all underlying hookimpls + # we invoke the HookCaller and thus all underlying hookimpls result_list = pm.hook.myhook(config=config, args=sys.argv) Note that you **must** call hooks using keyword :std:term:`python:argument` syntax! @@ -723,7 +845,7 @@ Collecting results ------------------ By default calling a hook results in all underlying :ref:`hookimpls <impls>` functions to be invoked in sequence via a loop. Any function -which returns a value other then a ``None`` result will have that result +which returns a value other than a ``None`` result will have that result appended to a :py:class:`list` which is returned by the call. The only exception to this behaviour is if the hook has been marked to return @@ -734,10 +856,9 @@ single value (which is not ``None``) will be returned. Exception handling ------------------ -If any *hookimpl* errors with an exception no further callbacks -are invoked and the exception is packaged up and delivered to -any :ref:`wrappers <hookwrappers>` before being re-raised at the -hook invocation point: +If any *hookimpl* errors with an exception no further callbacks are invoked and +the exception is delivered to any :ref:`wrappers <hookwrappers>` before being +re-raised at the hook invocation point: .. code-block:: python @@ -764,15 +885,14 @@ hook invocation point: return 3 - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def myhook(self, args): - outcome = yield - try: - outcome.get_result() - except RuntimeError: - # log the error details - print(outcome.excinfo) + return (yield) + except RuntimeError as exc: + # log runtime error details + print(exc) + raise pm = PluginManager("myproject") @@ -796,7 +916,7 @@ only useful if you expect that some *hookimpls* may be registered **after** the hook is initially invoked. Historic hooks must be :ref:`specially marked <historic>` and called -using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method: +using the :py:meth:`~pluggy.HookCaller.call_historic()` method: .. code-block:: python @@ -817,8 +937,8 @@ using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method: # historic callback is invoked here pm.register(mylateplugin) -Note that if you :py:meth:`~pluggy._hooks._HookCaller.call_historic()` -the :py:class:`~pluggy._hooks._HookCaller` (and thus your calling code) +Note that if you :py:meth:`~pluggy.HookCaller.call_historic()` +the :py:class:`~pluggy.HookCaller` (and thus your calling code) can not receive results back from the underlying *hookimpl* functions. Instead you can provide a *callback* for processing results (like the ``callback`` function above) which will be called as each new plugin @@ -829,23 +949,28 @@ is registered. hooks since only the first registered plugin's hook(s) would ever be called. +.. _call_extra: + Calling with extras ------------------- You can call a hook with temporarily participating *implementation* functions (that aren't in the registry) using the -:py:meth:`pluggy._hooks._HookCaller.call_extra()` method. +:py:meth:`pluggy.HookCaller.call_extra()` method. Calling with a subset of registered plugins ------------------------------------------- You can make a call using a subset of plugins by asking the :py:class:`~pluggy.PluginManager` first for a -:py:class:`~pluggy._hooks._HookCaller` with those plugins removed +:py:class:`~pluggy.HookCaller` with those plugins removed using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method. -You then can use that :py:class:`_HookCaller <pluggy._hooks._HookCaller>` -to make normal, :py:meth:`~pluggy._hooks._HookCaller.call_historic`, or -:py:meth:`~pluggy._hooks._HookCaller.call_extra` calls as necessary. +You then can use that :py:class:`~pluggy.HookCaller` +to make normal, :py:meth:`~pluggy.HookCaller.call_historic`, or +:py:meth:`~pluggy.HookCaller.call_extra` calls as necessary. + + +.. _tracing: Built-in tracing **************** @@ -877,11 +1002,11 @@ The expected signature and default implementations for these functions is: .. code-block:: python - def before(hook_name, methods, kwargs): + def before(hook_name, hook_impls, kwargs): pass - def after(outcome, hook_name, methods, kwargs): + def after(outcome, hook_name, hook_impls, kwargs): pass Public API @@ -939,13 +1064,13 @@ Table of contents .. _callbacks: https://en.wikipedia.org/wiki/Callback_(computer_programming) .. _tox test suite: - https://github.com/pytest-dev/pluggy/blob/master/tox.ini + https://github.com/pytest-dev/pluggy/blob/main/tox.ini .. _Semantic Versioning: https://semver.org/ .. _Python interpreters: - https://github.com/pytest-dev/pluggy/blob/master/tox.ini#L2 -.. _500+ plugins: - http://plugincompat.herokuapp.com/ + https://github.com/pytest-dev/pluggy/blob/main/tox.ini#L2 +.. _1400+ plugins: + https://docs.pytest.org/en/latest/reference/plugin_list.html .. _pre-commit: https://pre-commit.com/ diff --git a/tests/wpt/tests/tools/third_party/pluggy/docs/requirements.txt b/tests/wpt/tests/tools/third_party/pluggy/docs/requirements.txt new file mode 100644 index 00000000000..7d0b87a35ec --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/docs/requirements.txt @@ -0,0 +1,4 @@ +# Higher bound for safety, can bump it if builds fine with new major versions. +sphinx>=6,<8 +pygments +towncrier diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/.gitignore b/tests/wpt/tests/tools/third_party/pluggy/downstream/.gitignore new file mode 100644 index 00000000000..0dc1814e369 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/.gitignore @@ -0,0 +1,6 @@ +/conda/ +/datasette/ +/devpi/ +/hatch/ +/pytest/ +/tox/ diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/README.md b/tests/wpt/tests/tools/third_party/pluggy/downstream/README.md new file mode 100644 index 00000000000..ff420e7d9d9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/README.md @@ -0,0 +1,2 @@ +This directory contains scripts for testing some downstream projects +against your current pluggy worktree. diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/conda.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/conda.sh new file mode 100755 index 00000000000..685d08d41b5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/conda.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d conda ]]; then + git clone https://github.com/conda/conda +fi +pushd conda && trap popd EXIT +git pull +set +eu +source dev/start +set -eu +pip install -e ../../ +pytest -m "not integration and not installed" diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/datasette.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/datasette.sh new file mode 100755 index 00000000000..7d3c5586b44 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/datasette.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d datasette ]]; then + git clone https://github.com/simonw/datasette +fi +pushd datasette && trap popd EXIT +git pull +python -m venv venv +venv/bin/pip install -e .[test] -e ../.. +venv/bin/pytest diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/devpi.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/devpi.sh new file mode 100755 index 00000000000..7ef09c8da07 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/devpi.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d devpi ]]; then + git clone https://github.com/devpi/devpi +fi +pushd devpi && trap popd EXIT +git pull +python -m venv venv +venv/bin/pip install -r dev-requirements.txt -e ../.. +venv/bin/pytest common +venv/bin/pytest server +venv/bin/pytest client +venv/bin/pytest web diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/hatch.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/hatch.sh new file mode 100755 index 00000000000..933e0f637b7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/hatch.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d hatch ]]; then + git clone https://github.com/pypa/hatch +fi +pushd hatch && trap popd EXIT +git pull +python -m venv venv +venv/bin/pip install -e . -e ./backend -e ../.. +venv/bin/hatch run dev diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/pytest.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/pytest.sh new file mode 100755 index 00000000000..5afc5612f0a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/pytest.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d pytest ]]; then + git clone https://github.com/pytest-dev/pytest +fi +pushd pytest && trap popd EXIT +git pull +python -m venv venv +venv/bin/pip install -e .[testing] -e ../.. +venv/bin/pytest diff --git a/tests/wpt/tests/tools/third_party/pluggy/downstream/tox.sh b/tests/wpt/tests/tools/third_party/pluggy/downstream/tox.sh new file mode 100755 index 00000000000..79e12dfa2b1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/downstream/tox.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -eux -o pipefail +if [[ ! -d tox ]]; then + git clone https://github.com/tox-dev/tox +fi +pushd tox && trap popd EXIT +python -m venv venv +venv/bin/pip install -e .[testing] -e ../.. +venv/bin/pytest diff --git a/tests/wpt/tests/tools/third_party/pluggy/pyproject.toml b/tests/wpt/tests/tools/third_party/pluggy/pyproject.toml index 15eba268983..e286825c4b4 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/pyproject.toml +++ b/tests/wpt/tests/tools/third_party/pluggy/pyproject.toml @@ -1,9 +1,24 @@ [build-system] requires = [ - "setuptools", - "setuptools-scm", - "wheel", + # sync with setup.py until we discard non-pep-517/518 + "setuptools>=45.0", + "setuptools-scm[toml]>=6.2.3", ] +build-backend = "setuptools.build_meta" + + +[tool.ruff.lint] +select = [ + "I", # isort +] + +[tool.ruff.lint.isort] +force-single-line = true +combine-as-imports = true +force-sort-within-sections = true +order-by-type = false +known-local-folder = ["pluggy"] +lines-after-imports = 2 [tool.setuptools_scm] write_to = "src/pluggy/_version.py" @@ -45,3 +60,25 @@ template = "changelog/_template.rst" directory = "trivial" name = "Trivial/Internal Changes" showcontent = true + +[tool.mypy] +mypy_path = "src" +check_untyped_defs = true +# Hopefully we can set this someday! +# disallow_any_expr = true +disallow_any_generics = true +disallow_any_unimported = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +ignore_missing_imports = true +implicit_reexport = false +no_implicit_optional = true +show_error_codes = true +strict_equality = true +strict_optional = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true diff --git a/tests/wpt/tests/tools/third_party/pluggy/scripts/release.py b/tests/wpt/tests/tools/third_party/pluggy/scripts/release.py index e09b8c77b11..879d35dfd4b 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/scripts/release.py +++ b/tests/wpt/tests/tools/third_party/pluggy/scripts/release.py @@ -1,12 +1,15 @@ """ Release script. """ + import argparse -import sys from subprocess import check_call +import sys -from colorama import init, Fore -from git import Repo, Remote +from colorama import Fore +from colorama import init +from git import Remote +from git import Repo def create_branch(version): @@ -50,7 +53,7 @@ def changelog(version, write_out=False): else: addopts = ["--draft"] print(f"{Fore.CYAN}Generating CHANGELOG") - check_call(["towncrier", "--yes", "--version", version] + addopts) + check_call(["towncrier", "build", "--yes", "--version", version] + addopts) def main(): diff --git a/tests/wpt/tests/tools/third_party/pluggy/scripts/towncrier-draft-to-file.py b/tests/wpt/tests/tools/third_party/pluggy/scripts/towncrier-draft-to-file.py new file mode 100644 index 00000000000..a47caa8fa8d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/scripts/towncrier-draft-to-file.py @@ -0,0 +1,16 @@ +from subprocess import call +import sys + + +def main(): + """ + Platform agnostic wrapper script for towncrier. + Fixes the issue (pytest#7251) where windows users are unable to natively + run tox -e docs to build pytest docs. + """ + with open("docs/_changelog_towncrier_draft.rst", "w") as draft_file: + return call(("towncrier", "--draft"), stdout=draft_file) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/wpt/tests/tools/third_party/pluggy/scripts/upload-coverage.sh b/tests/wpt/tests/tools/third_party/pluggy/scripts/upload-coverage.sh deleted file mode 100755 index ad3dd482814..00000000000 --- a/tests/wpt/tests/tools/third_party/pluggy/scripts/upload-coverage.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -if [ -z "$TOXENV" ]; then - python -m pip install coverage -else - # Add last TOXENV to $PATH. - PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" -fi - -python -m coverage xml -# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 -curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh -bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" diff --git a/tests/wpt/tests/tools/third_party/pluggy/setup.cfg b/tests/wpt/tests/tools/third_party/pluggy/setup.cfg index 7040bcb83be..1e34d361203 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/setup.cfg +++ b/tests/wpt/tests/tools/third_party/pluggy/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal=1 - [metadata] name = pluggy description = plugin and hook calling mechanisms for python @@ -25,17 +22,15 @@ classifiers = Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 [options] packages = pluggy -install_requires = - importlib-metadata>=0.12;python_version<"3.8" -python_requires = >=3.6 +python_requires = >=3.8 package_dir = =src setup_requires = @@ -47,6 +42,8 @@ dev = testing = pytest pytest-benchmark +[options.package_data] +pluggy = py.typed [devpi:upload] formats=sdist.tgz,bdist_wheel diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/__init__.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/__init__.py index 979028f759f..36ce1680621 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/__init__.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/__init__.py @@ -6,13 +6,32 @@ except ImportError: __version__ = "unknown" __all__ = [ + "__version__", "PluginManager", "PluginValidationError", + "HookCaller", "HookCallError", + "HookspecOpts", + "HookimplOpts", + "HookImpl", + "HookRelay", "HookspecMarker", "HookimplMarker", + "Result", + "PluggyWarning", + "PluggyTeardownRaisedWarning", ] -from ._manager import PluginManager, PluginValidationError -from ._callers import HookCallError -from ._hooks import HookspecMarker, HookimplMarker +from ._hooks import HookCaller +from ._hooks import HookImpl +from ._hooks import HookimplMarker +from ._hooks import HookimplOpts +from ._hooks import HookRelay +from ._hooks import HookspecMarker +from ._hooks import HookspecOpts +from ._manager import PluginManager +from ._manager import PluginValidationError +from ._result import HookCallError +from ._result import Result +from ._warnings import PluggyTeardownRaisedWarning +from ._warnings import PluggyWarning diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_callers.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_callers.py index 7a16f3bdd40..d01f925cca2 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_callers.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_callers.py @@ -1,22 +1,72 @@ """ Call loop machinery """ -import sys -from ._result import HookCallError, _Result, _raise_wrapfail +from __future__ import annotations +from typing import cast +from typing import Generator +from typing import Mapping +from typing import NoReturn +from typing import Sequence +from typing import Tuple +from typing import Union +import warnings -def _multicall(hook_name, hook_impls, caller_kwargs, firstresult): +from ._hooks import HookImpl +from ._result import HookCallError +from ._result import Result +from ._warnings import PluggyTeardownRaisedWarning + + +# Need to distinguish between old- and new-style hook wrappers. +# Wrapping with a tuple is the fastest type-safe way I found to do it. +Teardown = Union[ + Tuple[Generator[None, Result[object], None], HookImpl], + Generator[None, object, object], +] + + +def _raise_wrapfail( + wrap_controller: ( + Generator[None, Result[object], None] | Generator[None, object, object] + ), + msg: str, +) -> NoReturn: + co = wrap_controller.gi_code + raise RuntimeError( + "wrap_controller at %r %s:%d %s" + % (co.co_name, co.co_filename, co.co_firstlineno, msg) + ) + + +def _warn_teardown_exception( + hook_name: str, hook_impl: HookImpl, e: BaseException +) -> None: + msg = "A plugin raised an exception during an old-style hookwrapper teardown.\n" + msg += f"Plugin: {hook_impl.plugin_name}, Hook: {hook_name}\n" + msg += f"{type(e).__name__}: {e}\n" + msg += "For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning" # noqa: E501 + warnings.warn(PluggyTeardownRaisedWarning(msg), stacklevel=5) + + +def _multicall( + hook_name: str, + hook_impls: Sequence[HookImpl], + caller_kwargs: Mapping[str, object], + firstresult: bool, +) -> object | list[object]: """Execute a call into multiple python functions/methods and return the result(s). - ``caller_kwargs`` comes from _HookCaller.__call__(). + ``caller_kwargs`` comes from HookCaller.__call__(). """ __tracebackhide__ = True - results = [] - excinfo = None + results: list[object] = [] + exception = None + only_new_style_wrappers = True try: # run impl and wrapper setup functions in a loop - teardowns = [] + teardowns: list[Teardown] = [] try: for hook_impl in reversed(hook_impls): try: @@ -29,32 +79,104 @@ def _multicall(hook_name, hook_impls, caller_kwargs, firstresult): ) if hook_impl.hookwrapper: + only_new_style_wrappers = False + try: + # If this cast is not valid, a type error is raised below, + # which is the desired response. + res = hook_impl.function(*args) + wrapper_gen = cast(Generator[None, Result[object], None], res) + next(wrapper_gen) # first yield + teardowns.append((wrapper_gen, hook_impl)) + except StopIteration: + _raise_wrapfail(wrapper_gen, "did not yield") + elif hook_impl.wrapper: try: - gen = hook_impl.function(*args) - next(gen) # first yield - teardowns.append(gen) + # If this cast is not valid, a type error is raised below, + # which is the desired response. + res = hook_impl.function(*args) + function_gen = cast(Generator[None, object, object], res) + next(function_gen) # first yield + teardowns.append(function_gen) except StopIteration: - _raise_wrapfail(gen, "did not yield") + _raise_wrapfail(function_gen, "did not yield") else: res = hook_impl.function(*args) if res is not None: results.append(res) if firstresult: # halt further impl calls break - except BaseException: - excinfo = sys.exc_info() + except BaseException as exc: + exception = exc finally: - if firstresult: # first result hooks return a single value - outcome = _Result(results[0] if results else None, excinfo) + # Fast path - only new-style wrappers, no Result. + if only_new_style_wrappers: + if firstresult: # first result hooks return a single value + result = results[0] if results else None + else: + result = results + + # run all wrapper post-yield blocks + for teardown in reversed(teardowns): + try: + if exception is not None: + teardown.throw(exception) # type: ignore[union-attr] + else: + teardown.send(result) # type: ignore[union-attr] + # Following is unreachable for a well behaved hook wrapper. + # Try to force finalizers otherwise postponed till GC action. + # Note: close() may raise if generator handles GeneratorExit. + teardown.close() # type: ignore[union-attr] + except StopIteration as si: + result = si.value + exception = None + continue + except BaseException as e: + exception = e + continue + _raise_wrapfail(teardown, "has second yield") # type: ignore[arg-type] + + if exception is not None: + raise exception.with_traceback(exception.__traceback__) + else: + return result + + # Slow path - need to support old-style wrappers. else: - outcome = _Result(results, excinfo) + if firstresult: # first result hooks return a single value + outcome: Result[object | list[object]] = Result( + results[0] if results else None, exception + ) + else: + outcome = Result(results, exception) - # run all wrapper post-yield blocks - for gen in reversed(teardowns): - try: - gen.send(outcome) - _raise_wrapfail(gen, "has second yield") - except StopIteration: - pass + # run all wrapper post-yield blocks + for teardown in reversed(teardowns): + if isinstance(teardown, tuple): + try: + teardown[0].send(outcome) + except StopIteration: + pass + except BaseException as e: + _warn_teardown_exception(hook_name, teardown[1], e) + raise + else: + _raise_wrapfail(teardown[0], "has second yield") + else: + try: + if outcome._exception is not None: + teardown.throw(outcome._exception) + else: + teardown.send(outcome._result) + # Following is unreachable for a well behaved hook wrapper. + # Try to force finalizers otherwise postponed till GC action. + # Note: close() may raise if generator handles GeneratorExit. + teardown.close() + except StopIteration as si: + outcome.force_result(si.value) + continue + except BaseException as e: + outcome.force_exception(e) + continue + _raise_wrapfail(teardown, "has second yield") - return outcome.get_result() + return outcome.get_result() diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_hooks.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_hooks.py index 1e5fbb75958..362d791823e 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_hooks.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_hooks.py @@ -1,51 +1,160 @@ """ Internal hook annotation, representation and calling machinery. """ + +from __future__ import annotations + import inspect import sys +from types import ModuleType +from typing import AbstractSet +from typing import Any +from typing import Callable +from typing import Final +from typing import final +from typing import Generator +from typing import List +from typing import Mapping +from typing import Optional +from typing import overload +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING +from typing import TypedDict +from typing import TypeVar +from typing import Union import warnings - +from ._result import Result + + +_T = TypeVar("_T") +_F = TypeVar("_F", bound=Callable[..., object]) +_Namespace = Union[ModuleType, type] +_Plugin = object +_HookExec = Callable[ + [str, Sequence["HookImpl"], Mapping[str, object], bool], + Union[object, List[object]], +] +_HookImplFunction = Callable[..., Union[_T, Generator[None, Result[_T], None]]] + + +class HookspecOpts(TypedDict): + """Options for a hook specification.""" + + #: Whether the hook is :ref:`first result only <firstresult>`. + firstresult: bool + #: Whether the hook is :ref:`historic <historic>`. + historic: bool + #: Whether the hook :ref:`warns when implemented <warn_on_impl>`. + warn_on_impl: Warning | None + #: Whether the hook warns when :ref:`certain arguments are requested + #: <warn_on_impl>`. + #: + #: .. versionadded:: 1.5 + warn_on_impl_args: Mapping[str, Warning] | None + + +class HookimplOpts(TypedDict): + """Options for a hook implementation.""" + + #: Whether the hook implementation is a :ref:`wrapper <hookwrapper>`. + wrapper: bool + #: Whether the hook implementation is an :ref:`old-style wrapper + #: <old_style_hookwrappers>`. + hookwrapper: bool + #: Whether validation against a hook specification is :ref:`optional + #: <optionalhook>`. + optionalhook: bool + #: Whether to try to order this hook implementation :ref:`first + #: <callorder>`. + tryfirst: bool + #: Whether to try to order this hook implementation :ref:`last + #: <callorder>`. + trylast: bool + #: The name of the hook specification to match, see :ref:`specname`. + specname: str | None + + +@final class HookspecMarker: - """Decorator helper class for marking functions as hook specifications. + """Decorator for marking functions as hook specifications. - You can instantiate it with a project_name to get a decorator. - Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions - if the :py:class:`.PluginManager` uses the same project_name. + Instantiate it with a project_name to get a decorator. + Calling :meth:`PluginManager.add_hookspecs` later will discover all marked + functions if the :class:`PluginManager` uses the same project name. """ - def __init__(self, project_name): - self.project_name = project_name + __slots__ = ("project_name",) + + def __init__(self, project_name: str) -> None: + self.project_name: Final = project_name + @overload def __call__( - self, function=None, firstresult=False, historic=False, warn_on_impl=None - ): - """if passed a function, directly sets attributes on the function - which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`. - If passed no function, returns a decorator which can be applied to a function - later using the attributes supplied. + self, + function: _F, + firstresult: bool = False, + historic: bool = False, + warn_on_impl: Warning | None = None, + warn_on_impl_args: Mapping[str, Warning] | None = None, + ) -> _F: ... + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, + function: None = ..., + firstresult: bool = ..., + historic: bool = ..., + warn_on_impl: Warning | None = ..., + warn_on_impl_args: Mapping[str, Warning] | None = ..., + ) -> Callable[[_F], _F]: ... + + def __call__( # noqa: F811 + self, + function: _F | None = None, + firstresult: bool = False, + historic: bool = False, + warn_on_impl: Warning | None = None, + warn_on_impl_args: Mapping[str, Warning] | None = None, + ) -> _F | Callable[[_F], _F]: + """If passed a function, directly sets attributes on the function + which will make it discoverable to :meth:`PluginManager.add_hookspecs`. + + If passed no function, returns a decorator which can be applied to a + function later using the attributes supplied. + + :param firstresult: + If ``True``, the 1:N hook call (N being the number of registered + hook implementation functions) will stop at I<=N when the I'th + function returns a non-``None`` result. See :ref:`firstresult`. + + :param historic: + If ``True``, every call to the hook will be memorized and replayed + on plugins registered after the call was made. See :ref:`historic`. - If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered - hook implementation functions) will stop at I<=N when the I'th function - returns a non-``None`` result. + :param warn_on_impl: + If given, every implementation of this hook will trigger the given + warning. See :ref:`warn_on_impl`. - If ``historic`` is ``True`` calls to a hook will be memorized and replayed - on later registered plugins. + :param warn_on_impl_args: + If given, every implementation of this hook which requests one of + the arguments in the dict will trigger the corresponding warning. + See :ref:`warn_on_impl`. + .. versionadded:: 1.5 """ - def setattr_hookspec_opts(func): + def setattr_hookspec_opts(func: _F) -> _F: if historic and firstresult: raise ValueError("cannot have a historic firstresult hook") - setattr( - func, - self.project_name + "_spec", - dict( - firstresult=firstresult, - historic=historic, - warn_on_impl=warn_on_impl, - ), - ) + opts: HookspecOpts = { + "firstresult": firstresult, + "historic": historic, + "warn_on_impl": warn_on_impl, + "warn_on_impl_args": warn_on_impl_args, + } + setattr(func, self.project_name + "_spec", opts) return func if function is not None: @@ -54,65 +163,115 @@ class HookspecMarker: return setattr_hookspec_opts +@final class HookimplMarker: - """Decorator helper class for marking functions as hook implementations. + """Decorator for marking functions as hook implementations. - You can instantiate with a ``project_name`` to get a decorator. - Calling :py:meth:`.PluginManager.register` later will discover all marked functions - if the :py:class:`.PluginManager` uses the same project_name. + Instantiate it with a ``project_name`` to get a decorator. + Calling :meth:`PluginManager.register` later will discover all marked + functions if the :class:`PluginManager` uses the same project name. """ - def __init__(self, project_name): - self.project_name = project_name + __slots__ = ("project_name",) + + def __init__(self, project_name: str) -> None: + self.project_name: Final = project_name + @overload def __call__( self, - function=None, - hookwrapper=False, - optionalhook=False, - tryfirst=False, - trylast=False, - specname=None, - ): - - """if passed a function, directly sets attributes on the function - which will make it discoverable to :py:meth:`.PluginManager.register`. + function: _F, + hookwrapper: bool = ..., + optionalhook: bool = ..., + tryfirst: bool = ..., + trylast: bool = ..., + specname: str | None = ..., + wrapper: bool = ..., + ) -> _F: ... + + @overload # noqa: F811 + def __call__( # noqa: F811 + self, + function: None = ..., + hookwrapper: bool = ..., + optionalhook: bool = ..., + tryfirst: bool = ..., + trylast: bool = ..., + specname: str | None = ..., + wrapper: bool = ..., + ) -> Callable[[_F], _F]: ... + + def __call__( # noqa: F811 + self, + function: _F | None = None, + hookwrapper: bool = False, + optionalhook: bool = False, + tryfirst: bool = False, + trylast: bool = False, + specname: str | None = None, + wrapper: bool = False, + ) -> _F | Callable[[_F], _F]: + """If passed a function, directly sets attributes on the function + which will make it discoverable to :meth:`PluginManager.register`. + If passed no function, returns a decorator which can be applied to a function later using the attributes supplied. - If ``optionalhook`` is ``True`` a missing matching hook specification will not result - in an error (by default it is an error if no matching spec is found). - - If ``tryfirst`` is ``True`` this hook implementation will run as early as possible - in the chain of N hook implementations for a specification. - - If ``trylast`` is ``True`` this hook implementation will run as late as possible - in the chain of N hook implementations. - - If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly - one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper - function is run. The code after the ``yield`` is run after all non-hookwrapper - function have run. The ``yield`` receives a :py:class:`.callers._Result` object - representing the exception or result outcome of the inner calls (including other - hookwrapper calls). - - If ``specname`` is provided, it will be used instead of the function name when - matching this hook implementation to a hook specification during registration. - + :param optionalhook: + If ``True``, a missing matching hook specification will not result + in an error (by default it is an error if no matching spec is + found). See :ref:`optionalhook`. + + :param tryfirst: + If ``True``, this hook implementation will run as early as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param trylast: + If ``True``, this hook implementation will run as late as possible + in the chain of N hook implementations for a specification. See + :ref:`callorder`. + + :param wrapper: + If ``True`` ("new-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + functions have run. The ``yield`` receives the result value of the + inner calls, or raises the exception of inner calls (including + earlier hook wrapper calls). The return value of the function + becomes the return value of the hook, and a raised exception becomes + the exception of the hook. See :ref:`hookwrapper`. + + :param hookwrapper: + If ``True`` ("old-style hook wrapper"), the hook implementation + needs to execute exactly one ``yield``. The code before the + ``yield`` is run early before any non-hook-wrapper function is run. + The code after the ``yield`` is run after all non-hook-wrapper + function have run The ``yield`` receives a :class:`Result` object + representing the exception or result outcome of the inner calls + (including earlier hook wrapper calls). This option is mutually + exclusive with ``wrapper``. See :ref:`old_style_hookwrapper`. + + :param specname: + If provided, the given name will be used instead of the function + name when matching this hook implementation to a hook specification + during registration. See :ref:`specname`. + + .. versionadded:: 1.2.0 + The ``wrapper`` parameter. """ - def setattr_hookimpl_opts(func): - setattr( - func, - self.project_name + "_impl", - dict( - hookwrapper=hookwrapper, - optionalhook=optionalhook, - tryfirst=tryfirst, - trylast=trylast, - specname=specname, - ), - ) + def setattr_hookimpl_opts(func: _F) -> _F: + opts: HookimplOpts = { + "wrapper": wrapper, + "hookwrapper": hookwrapper, + "optionalhook": optionalhook, + "tryfirst": tryfirst, + "trylast": trylast, + "specname": specname, + } + setattr(func, self.project_name + "_impl", opts) return func if function is None: @@ -121,9 +280,10 @@ class HookimplMarker: return setattr_hookimpl_opts(function) -def normalize_hookimpl_opts(opts): +def normalize_hookimpl_opts(opts: HookimplOpts) -> None: opts.setdefault("tryfirst", False) opts.setdefault("trylast", False) + opts.setdefault("wrapper", False) opts.setdefault("hookwrapper", False) opts.setdefault("optionalhook", False) opts.setdefault("specname", None) @@ -132,7 +292,7 @@ def normalize_hookimpl_opts(opts): _PYPY = hasattr(sys, "pypy_version_info") -def varnames(func): +def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: """Return tuple of positional and keywrord argument names for a function, method, class or callable. @@ -150,12 +310,33 @@ def varnames(func): except Exception: return (), () - try: # func MUST be a function or method here or we won't parse any args - spec = inspect.getfullargspec(func) + try: + # func MUST be a function or method here or we won't parse any args. + sig = inspect.signature( + func.__func__ if inspect.ismethod(func) else func # type:ignore[arg-type] + ) except TypeError: return (), () - args, defaults = tuple(spec.args), spec.defaults + _valid_param_kinds = ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) + _valid_params = { + name: param + for name, param in sig.parameters.items() + if param.kind in _valid_param_kinds + } + args = tuple(_valid_params) + defaults = ( + tuple( + param.default + for param in _valid_params.values() + if param.default is not param.empty + ) + or None + ) + if defaults: index = -len(defaults) args, kwargs = args[:index], tuple(args[index:]) @@ -164,162 +345,371 @@ def varnames(func): # strip any implicit instance arg # pypy3 uses "obj" instead of "self" for default dunder methods - implicit_names = ("self",) if not _PYPY else ("self", "obj") + if not _PYPY: + implicit_names: tuple[str, ...] = ("self",) + else: + implicit_names = ("self", "obj") if args: - if inspect.ismethod(func) or ( - "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names - ): + qualname: str = getattr(func, "__qualname__", "") + if inspect.ismethod(func) or ("." in qualname and args[0] in implicit_names): args = args[1:] return args, kwargs -class _HookRelay: - """hook holder object for performing 1:N hook calls where N is the number - of registered plugins. +@final +class HookRelay: + """Hook holder object for performing 1:N hook calls where N is the number + of registered plugins.""" - """ + __slots__ = ("__dict__",) + def __init__(self) -> None: + """:meta private:""" -class _HookCaller: - def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None): - self.name = name - self._wrappers = [] - self._nonwrappers = [] - self._hookexec = hook_execute - self._call_history = None - self.spec = None + if TYPE_CHECKING: + + def __getattr__(self, name: str) -> HookCaller: ... + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookRelay = HookRelay + + +_CallHistory = List[Tuple[Mapping[str, object], Optional[Callable[[Any], None]]]] + + +class HookCaller: + """A caller of all registered implementations of a hook specification.""" + + __slots__ = ( + "name", + "spec", + "_hookexec", + "_hookimpls", + "_call_history", + ) + + def __init__( + self, + name: str, + hook_execute: _HookExec, + specmodule_or_class: _Namespace | None = None, + spec_opts: HookspecOpts | None = None, + ) -> None: + """:meta private:""" + #: Name of the hook getting called. + self.name: Final = name + self._hookexec: Final = hook_execute + # The hookimpls list. The caller iterates it *in reverse*. Format: + # 1. trylast nonwrappers + # 2. nonwrappers + # 3. tryfirst nonwrappers + # 4. trylast wrappers + # 5. wrappers + # 6. tryfirst wrappers + self._hookimpls: Final[list[HookImpl]] = [] + self._call_history: _CallHistory | None = None + # TODO: Document, or make private. + self.spec: HookSpec | None = None if specmodule_or_class is not None: assert spec_opts is not None self.set_specification(specmodule_or_class, spec_opts) - def has_spec(self): + # TODO: Document, or make private. + def has_spec(self) -> bool: return self.spec is not None - def set_specification(self, specmodule_or_class, spec_opts): - assert not self.has_spec() + # TODO: Document, or make private. + def set_specification( + self, + specmodule_or_class: _Namespace, + spec_opts: HookspecOpts, + ) -> None: + if self.spec is not None: + raise ValueError( + f"Hook {self.spec.name!r} is already registered " + f"within namespace {self.spec.namespace}" + ) self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) if spec_opts.get("historic"): self._call_history = [] - def is_historic(self): + def is_historic(self) -> bool: + """Whether this caller is :ref:`historic <historic>`.""" return self._call_history is not None - def _remove_plugin(self, plugin): - def remove(wrappers): - for i, method in enumerate(wrappers): - if method.plugin == plugin: - del wrappers[i] - return True - - if remove(self._wrappers) is None: - if remove(self._nonwrappers) is None: - raise ValueError(f"plugin {plugin!r} not found") + def _remove_plugin(self, plugin: _Plugin) -> None: + for i, method in enumerate(self._hookimpls): + if method.plugin == plugin: + del self._hookimpls[i] + return + raise ValueError(f"plugin {plugin!r} not found") - def get_hookimpls(self): - # Order is important for _hookexec - return self._nonwrappers + self._wrappers + def get_hookimpls(self) -> list[HookImpl]: + """Get all registered hook implementations for this hook.""" + return self._hookimpls.copy() - def _add_hookimpl(self, hookimpl): + def _add_hookimpl(self, hookimpl: HookImpl) -> None: """Add an implementation to the callback chain.""" - if hookimpl.hookwrapper: - methods = self._wrappers + for i, method in enumerate(self._hookimpls): + if method.hookwrapper or method.wrapper: + splitpoint = i + break else: - methods = self._nonwrappers + splitpoint = len(self._hookimpls) + if hookimpl.hookwrapper or hookimpl.wrapper: + start, end = splitpoint, len(self._hookimpls) + else: + start, end = 0, splitpoint if hookimpl.trylast: - methods.insert(0, hookimpl) + self._hookimpls.insert(start, hookimpl) elif hookimpl.tryfirst: - methods.append(hookimpl) + self._hookimpls.insert(end, hookimpl) else: # find last non-tryfirst method - i = len(methods) - 1 - while i >= 0 and methods[i].tryfirst: + i = end - 1 + while i >= start and self._hookimpls[i].tryfirst: i -= 1 - methods.insert(i + 1, hookimpl) - - def __repr__(self): - return f"<_HookCaller {self.name!r}>" + self._hookimpls.insert(i + 1, hookimpl) - def __call__(self, *args, **kwargs): - if args: - raise TypeError("hook calling supports only keyword arguments") - assert not self.is_historic() + def __repr__(self) -> str: + return f"<HookCaller {self.name!r}>" + def _verify_all_args_are_provided(self, kwargs: Mapping[str, object]) -> None: # This is written to avoid expensive operations when not needed. if self.spec: for argname in self.spec.argnames: if argname not in kwargs: - notincall = tuple(set(self.spec.argnames) - kwargs.keys()) + notincall = ", ".join( + repr(argname) + for argname in self.spec.argnames + # Avoid self.spec.argnames - kwargs.keys() - doesn't preserve order. + if argname not in kwargs.keys() + ) warnings.warn( "Argument(s) {} which are declared in the hookspec " - "can not be found in this hook call".format(notincall), + "cannot be found in this hook call".format(notincall), stacklevel=2, ) break - firstresult = self.spec.opts.get("firstresult") - else: - firstresult = False + def __call__(self, **kwargs: object) -> Any: + """Call the hook. - return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult) + Only accepts keyword arguments, which should match the hook + specification. - def call_historic(self, result_callback=None, kwargs=None): + Returns the result(s) of calling all registered plugins, see + :ref:`calling`. + """ + assert ( + not self.is_historic() + ), "Cannot directly call a historic hook - use call_historic instead." + self._verify_all_args_are_provided(kwargs) + firstresult = self.spec.opts.get("firstresult", False) if self.spec else False + # Copy because plugins may register other plugins during iteration (#438). + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + + def call_historic( + self, + result_callback: Callable[[Any], None] | None = None, + kwargs: Mapping[str, object] | None = None, + ) -> None: """Call the hook with given ``kwargs`` for all registered plugins and - for all plugins which will be registered afterwards. + for all plugins which will be registered afterwards, see + :ref:`historic`. - If ``result_callback`` is not ``None`` it will be called for for each - non-``None`` result obtained from a hook implementation. + :param result_callback: + If provided, will be called for each non-``None`` result obtained + from a hook implementation. """ - self._call_history.append((kwargs or {}, result_callback)) + assert self._call_history is not None + kwargs = kwargs or {} + self._verify_all_args_are_provided(kwargs) + self._call_history.append((kwargs, result_callback)) # Historizing hooks don't return results. # Remember firstresult isn't compatible with historic. - res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False) + # Copy because plugins may register other plugins during iteration (#438). + res = self._hookexec(self.name, self._hookimpls.copy(), kwargs, False) if result_callback is None: return - for x in res or []: - result_callback(x) + if isinstance(res, list): + for x in res: + result_callback(x) - def call_extra(self, methods, kwargs): + def call_extra( + self, methods: Sequence[Callable[..., object]], kwargs: Mapping[str, object] + ) -> Any: """Call the hook with some additional temporarily participating - methods using the specified ``kwargs`` as call parameters.""" - old = list(self._nonwrappers), list(self._wrappers) + methods using the specified ``kwargs`` as call parameters, see + :ref:`call_extra`.""" + assert ( + not self.is_historic() + ), "Cannot directly call a historic hook - use call_historic instead." + self._verify_all_args_are_provided(kwargs) + opts: HookimplOpts = { + "wrapper": False, + "hookwrapper": False, + "optionalhook": False, + "trylast": False, + "tryfirst": False, + "specname": None, + } + hookimpls = self._hookimpls.copy() for method in methods: - opts = dict(hookwrapper=False, trylast=False, tryfirst=False) hookimpl = HookImpl(None, "<temp>", method, opts) - self._add_hookimpl(hookimpl) - try: - return self(**kwargs) - finally: - self._nonwrappers, self._wrappers = old + # Find last non-tryfirst nonwrapper method. + i = len(hookimpls) - 1 + while i >= 0 and ( + # Skip wrappers. + (hookimpls[i].hookwrapper or hookimpls[i].wrapper) + # Skip tryfirst nonwrappers. + or hookimpls[i].tryfirst + ): + i -= 1 + hookimpls.insert(i + 1, hookimpl) + firstresult = self.spec.opts.get("firstresult", False) if self.spec else False + return self._hookexec(self.name, hookimpls, kwargs, firstresult) - def _maybe_apply_history(self, method): + def _maybe_apply_history(self, method: HookImpl) -> None: """Apply call history to a new hookimpl if it is marked as historic.""" if self.is_historic(): + assert self._call_history is not None for kwargs, result_callback in self._call_history: res = self._hookexec(self.name, [method], kwargs, False) if res and result_callback is not None: + # XXX: remember firstresult isn't compat with historic + assert isinstance(res, list) result_callback(res[0]) -class HookImpl: - def __init__(self, plugin, plugin_name, function, hook_impl_opts): - self.function = function - self.argnames, self.kwargnames = varnames(self.function) - self.plugin = plugin - self.opts = hook_impl_opts - self.plugin_name = plugin_name - self.__dict__.update(hook_impl_opts) +# Historical name (pluggy<=1.2), kept for backward compatibility. +_HookCaller = HookCaller + + +class _SubsetHookCaller(HookCaller): + """A proxy to another HookCaller which manages calls to all registered + plugins except the ones from remove_plugins.""" + + # This class is unusual: in inhertits from `HookCaller` so all of + # the *code* runs in the class, but it delegates all underlying *data* + # to the original HookCaller. + # `subset_hook_caller` used to be implemented by creating a full-fledged + # HookCaller, copying all hookimpls from the original. This had problems + # with memory leaks (#346) and historic calls (#347), which make a proxy + # approach better. + # An alternative implementation is to use a `_getattr__`/`__getattribute__` + # proxy, however that adds more overhead and is more tricky to implement. + + __slots__ = ( + "_orig", + "_remove_plugins", + ) + + def __init__(self, orig: HookCaller, remove_plugins: AbstractSet[_Plugin]) -> None: + self._orig = orig + self._remove_plugins = remove_plugins + self.name = orig.name # type: ignore[misc] + self._hookexec = orig._hookexec # type: ignore[misc] + + @property # type: ignore[misc] + def _hookimpls(self) -> list[HookImpl]: + return [ + impl + for impl in self._orig._hookimpls + if impl.plugin not in self._remove_plugins + ] - def __repr__(self): + @property + def spec(self) -> HookSpec | None: # type: ignore[override] + return self._orig.spec + + @property + def _call_history(self) -> _CallHistory | None: # type: ignore[override] + return self._orig._call_history + + def __repr__(self) -> str: + return f"<_SubsetHookCaller {self.name!r}>" + + +@final +class HookImpl: + """A hook implementation in a :class:`HookCaller`.""" + + __slots__ = ( + "function", + "argnames", + "kwargnames", + "plugin", + "opts", + "plugin_name", + "wrapper", + "hookwrapper", + "optionalhook", + "tryfirst", + "trylast", + ) + + def __init__( + self, + plugin: _Plugin, + plugin_name: str, + function: _HookImplFunction[object], + hook_impl_opts: HookimplOpts, + ) -> None: + """:meta private:""" + #: The hook implementation function. + self.function: Final = function + argnames, kwargnames = varnames(self.function) + #: The positional parameter names of ``function```. + self.argnames: Final = argnames + #: The keyword parameter names of ``function```. + self.kwargnames: Final = kwargnames + #: The plugin which defined this hook implementation. + self.plugin: Final = plugin + #: The :class:`HookimplOpts` used to configure this hook implementation. + self.opts: Final = hook_impl_opts + #: The name of the plugin which defined this hook implementation. + self.plugin_name: Final = plugin_name + #: Whether the hook implementation is a :ref:`wrapper <hookwrapper>`. + self.wrapper: Final = hook_impl_opts["wrapper"] + #: Whether the hook implementation is an :ref:`old-style wrapper + #: <old_style_hookwrappers>`. + self.hookwrapper: Final = hook_impl_opts["hookwrapper"] + #: Whether validation against a hook specification is :ref:`optional + #: <optionalhook>`. + self.optionalhook: Final = hook_impl_opts["optionalhook"] + #: Whether to try to order this hook implementation :ref:`first + #: <callorder>`. + self.tryfirst: Final = hook_impl_opts["tryfirst"] + #: Whether to try to order this hook implementation :ref:`last + #: <callorder>`. + self.trylast: Final = hook_impl_opts["trylast"] + + def __repr__(self) -> str: return f"<HookImpl plugin_name={self.plugin_name!r}, plugin={self.plugin!r}>" +@final class HookSpec: - def __init__(self, namespace, name, opts): + __slots__ = ( + "namespace", + "function", + "name", + "argnames", + "kwargnames", + "opts", + "warn_on_impl", + "warn_on_impl_args", + ) + + def __init__(self, namespace: _Namespace, name: str, opts: HookspecOpts) -> None: self.namespace = namespace - self.function = function = getattr(namespace, name) + self.function: Callable[..., object] = getattr(namespace, name) self.name = name - self.argnames, self.kwargnames = varnames(function) + self.argnames, self.kwargnames = varnames(self.function) self.opts = opts self.warn_on_impl = opts.get("warn_on_impl") + self.warn_on_impl_args = opts.get("warn_on_impl_args") diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_manager.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_manager.py index 65f4e508427..9998dd815b5 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_manager.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_manager.py @@ -1,95 +1,150 @@ +from __future__ import annotations + import inspect -import sys +import types +from typing import Any +from typing import Callable +from typing import cast +from typing import Final +from typing import Iterable +from typing import Mapping +from typing import Sequence +from typing import TYPE_CHECKING import warnings from . import _tracing -from ._callers import _Result, _multicall -from ._hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts +from ._callers import _multicall +from ._hooks import _HookImplFunction +from ._hooks import _Namespace +from ._hooks import _Plugin +from ._hooks import _SubsetHookCaller +from ._hooks import HookCaller +from ._hooks import HookImpl +from ._hooks import HookimplOpts +from ._hooks import HookRelay +from ._hooks import HookspecOpts +from ._hooks import normalize_hookimpl_opts +from ._result import Result + + +if TYPE_CHECKING: + # importtlib.metadata import is slow, defer it. + import importlib.metadata + -if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata -else: - import importlib_metadata +_BeforeTrace = Callable[[str, Sequence[HookImpl], Mapping[str, Any]], None] +_AfterTrace = Callable[[Result[Any], str, Sequence[HookImpl], Mapping[str, Any]], None] -def _warn_for_function(warning, function): +def _warn_for_function(warning: Warning, function: Callable[..., object]) -> None: + func = cast(types.FunctionType, function) warnings.warn_explicit( warning, type(warning), - lineno=function.__code__.co_firstlineno, - filename=function.__code__.co_filename, + lineno=func.__code__.co_firstlineno, + filename=func.__code__.co_filename, ) class PluginValidationError(Exception): - """plugin failed validation. + """Plugin failed validation. - :param object plugin: the plugin which failed validation, - may be a module or an arbitrary object. + :param plugin: The plugin which failed validation. + :param message: Error message. """ - def __init__(self, plugin, message): + def __init__(self, plugin: _Plugin, message: str) -> None: + super().__init__(message) + #: The plugin which failed validation. self.plugin = plugin - super(Exception, self).__init__(message) class DistFacade: """Emulate a pkg_resources Distribution""" - def __init__(self, dist): + def __init__(self, dist: importlib.metadata.Distribution) -> None: self._dist = dist @property - def project_name(self): - return self.metadata["name"] + def project_name(self) -> str: + name: str = self.metadata["name"] + return name - def __getattr__(self, attr, default=None): + def __getattr__(self, attr: str, default=None): return getattr(self._dist, attr, default) - def __dir__(self): + def __dir__(self) -> list[str]: return sorted(dir(self._dist) + ["_dist", "project_name"]) class PluginManager: - """Core :py:class:`.PluginManager` class which manages registration - of plugin objects and 1:N hook calling. + """Core class which manages registration of plugin objects and 1:N hook + calling. - You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class) - <.PluginManager.add_hookspecs>`. - You can register plugin objects (which contain hooks) by calling - :py:meth:`register(plugin) <.PluginManager.register>`. The :py:class:`.PluginManager` - is initialized with a prefix that is searched for in the names of the dict - of registered plugin objects. + You can register new hooks by calling :meth:`add_hookspecs(module_or_class) + <PluginManager.add_hookspecs>`. - For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing` + You can register plugin objects (which contain hook implementations) by + calling :meth:`register(plugin) <PluginManager.register>`. + + For debugging purposes you can call :meth:`PluginManager.enable_tracing` which will subsequently send debug information to the trace helper. + + :param project_name: + The short project name. Prefer snake case. Make sure it's unique! """ - def __init__(self, project_name): - self.project_name = project_name - self._name2plugin = {} - self._plugin2hookcallers = {} - self._plugin_distinfo = [] - self.trace = _tracing.TagTracer().get("pluginmanage") - self.hook = _HookRelay() + def __init__(self, project_name: str) -> None: + #: The project name. + self.project_name: Final = project_name + self._name2plugin: Final[dict[str, _Plugin]] = {} + self._plugin_distinfo: Final[list[tuple[_Plugin, DistFacade]]] = [] + #: The "hook relay", used to call a hook on all registered plugins. + #: See :ref:`calling`. + self.hook: Final = HookRelay() + #: The tracing entry point. See :ref:`tracing`. + self.trace: Final[_tracing.TagTracerSub] = _tracing.TagTracer().get( + "pluginmanage" + ) self._inner_hookexec = _multicall - def _hookexec(self, hook_name, methods, kwargs, firstresult): + def _hookexec( + self, + hook_name: str, + methods: Sequence[HookImpl], + kwargs: Mapping[str, object], + firstresult: bool, + ) -> object | list[object]: # called from all hookcaller instances. # enable_tracing will set its own wrapping function at self._inner_hookexec return self._inner_hookexec(hook_name, methods, kwargs, firstresult) - def register(self, plugin, name=None): - """Register a plugin and return its canonical name or ``None`` if the name - is blocked from registering. Raise a :py:class:`ValueError` if the plugin - is already registered.""" + def register(self, plugin: _Plugin, name: str | None = None) -> str | None: + """Register a plugin and return its name. + + :param name: + The name under which to register the plugin. If not specified, a + name is generated using :func:`get_canonical_name`. + + :returns: + The plugin name. If the name is blocked from registering, returns + ``None``. + + If the plugin is already registered, raises a :exc:`ValueError`. + """ plugin_name = name or self.get_canonical_name(plugin) - if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if plugin_name in self._name2plugin: if self._name2plugin.get(plugin_name, -1) is None: - return # blocked plugin, return None to indicate no registration + return None # blocked plugin, return None to indicate no registration + raise ValueError( + "Plugin name already registered: %s=%s\n%s" + % (plugin_name, plugin, self._name2plugin) + ) + + if plugin in self._name2plugin.values(): raise ValueError( - "Plugin already registered: %s=%s\n%s" + "Plugin already registered under a different name: %s=%s\n%s" % (plugin_name, plugin, self._name2plugin) ) @@ -98,79 +153,115 @@ class PluginManager: self._name2plugin[plugin_name] = plugin # register matching hook implementations of the plugin - self._plugin2hookcallers[plugin] = hookcallers = [] for name in dir(plugin): hookimpl_opts = self.parse_hookimpl_opts(plugin, name) if hookimpl_opts is not None: normalize_hookimpl_opts(hookimpl_opts) - method = getattr(plugin, name) + method: _HookImplFunction[object] = getattr(plugin, name) hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) name = hookimpl_opts.get("specname") or name - hook = getattr(self.hook, name, None) + hook: HookCaller | None = getattr(self.hook, name, None) if hook is None: - hook = _HookCaller(name, self._hookexec) + hook = HookCaller(name, self._hookexec) setattr(self.hook, name, hook) elif hook.has_spec(): self._verify_hook(hook, hookimpl) hook._maybe_apply_history(hookimpl) hook._add_hookimpl(hookimpl) - hookcallers.append(hook) return plugin_name - def parse_hookimpl_opts(self, plugin, name): - method = getattr(plugin, name) + def parse_hookimpl_opts(self, plugin: _Plugin, name: str) -> HookimplOpts | None: + """Try to obtain a hook implementation from an item with the given name + in the given plugin which is being searched for hook impls. + + :returns: + The parsed hookimpl options, or None to skip the given item. + + This method can be overridden by ``PluginManager`` subclasses to + customize how hook implementation are picked up. By default, returns the + options for items decorated with :class:`HookimplMarker`. + """ + method: object = getattr(plugin, name) if not inspect.isroutine(method): - return + return None try: - res = getattr(method, self.project_name + "_impl", None) + res: HookimplOpts | None = getattr( + method, self.project_name + "_impl", None + ) except Exception: - res = {} + res = {} # type: ignore[assignment] if res is not None and not isinstance(res, dict): # false positive - res = None + res = None # type:ignore[unreachable] return res - def unregister(self, plugin=None, name=None): - """unregister a plugin object and all its contained hook implementations - from internal data structures.""" + def unregister( + self, plugin: _Plugin | None = None, name: str | None = None + ) -> Any | None: + """Unregister a plugin and all of its hook implementations. + + The plugin can be specified either by the plugin object or the plugin + name. If both are specified, they must agree. + + Returns the unregistered plugin, or ``None`` if not found. + """ if name is None: assert plugin is not None, "one of name or plugin needs to be specified" name = self.get_name(plugin) + assert name is not None, "plugin is not registered" if plugin is None: plugin = self.get_plugin(name) + if plugin is None: + return None + + hookcallers = self.get_hookcallers(plugin) + if hookcallers: + for hookcaller in hookcallers: + hookcaller._remove_plugin(plugin) # if self._name2plugin[name] == None registration was blocked: ignore if self._name2plugin.get(name): + assert name is not None del self._name2plugin[name] - for hookcaller in self._plugin2hookcallers.pop(plugin, []): - hookcaller._remove_plugin(plugin) - return plugin - def set_blocked(self, name): - """block registrations of the given name, unregister if already registered.""" + def set_blocked(self, name: str) -> None: + """Block registrations of the given name, unregister if already registered.""" self.unregister(name=name) self._name2plugin[name] = None - def is_blocked(self, name): - """return ``True`` if the given plugin name is blocked.""" + def is_blocked(self, name: str) -> bool: + """Return whether the given plugin name is blocked.""" return name in self._name2plugin and self._name2plugin[name] is None - def add_hookspecs(self, module_or_class): - """add new hook specifications defined in the given ``module_or_class``. - Functions are recognized if they have been decorated accordingly.""" + def unblock(self, name: str) -> bool: + """Unblocks a name. + + Returns whether the name was actually blocked. + """ + if self._name2plugin.get(name, -1) is None: + del self._name2plugin[name] + return True + return False + + def add_hookspecs(self, module_or_class: _Namespace) -> None: + """Add new hook specifications defined in the given ``module_or_class``. + + Functions are recognized as hook specifications if they have been + decorated with a matching :class:`HookspecMarker`. + """ names = [] for name in dir(module_or_class): spec_opts = self.parse_hookspec_opts(module_or_class, name) if spec_opts is not None: - hc = getattr(self.hook, name, None) + hc: HookCaller | None = getattr(self.hook, name, None) if hc is None: - hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts) + hc = HookCaller(name, self._hookexec, module_or_class, spec_opts) setattr(self.hook, name, hc) else: - # plugins registered this hook without knowing the spec + # Plugins registered this hook without knowing the spec. hc.set_specification(module_or_class, spec_opts) for hookfunction in hc.get_hookimpls(): self._verify_hook(hc, hookfunction) @@ -181,48 +272,68 @@ class PluginManager: f"did not find any {self.project_name!r} hooks in {module_or_class!r}" ) - def parse_hookspec_opts(self, module_or_class, name): + def parse_hookspec_opts( + self, module_or_class: _Namespace, name: str + ) -> HookspecOpts | None: + """Try to obtain a hook specification from an item with the given name + in the given module or class which is being searched for hook specs. + + :returns: + The parsed hookspec options for defining a hook, or None to skip the + given item. + + This method can be overridden by ``PluginManager`` subclasses to + customize how hook specifications are picked up. By default, returns the + options for items decorated with :class:`HookspecMarker`. + """ method = getattr(module_or_class, name) - return getattr(method, self.project_name + "_spec", None) - - def get_plugins(self): - """return the set of registered plugins.""" - return set(self._plugin2hookcallers) - - def is_registered(self, plugin): - """Return ``True`` if the plugin is already registered.""" - return plugin in self._plugin2hookcallers - - def get_canonical_name(self, plugin): - """Return canonical name for a plugin object. Note that a plugin - may be registered under a different name which was specified - by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`. - To obtain the name of an registered plugin use :py:meth:`get_name(plugin) - <.PluginManager.get_name>` instead.""" - return getattr(plugin, "__name__", None) or str(id(plugin)) - - def get_plugin(self, name): - """Return a plugin or ``None`` for the given name.""" + opts: HookspecOpts | None = getattr(method, self.project_name + "_spec", None) + return opts + + def get_plugins(self) -> set[Any]: + """Return a set of all registered plugin objects.""" + return {x for x in self._name2plugin.values() if x is not None} + + def is_registered(self, plugin: _Plugin) -> bool: + """Return whether the plugin is already registered.""" + return any(plugin == val for val in self._name2plugin.values()) + + def get_canonical_name(self, plugin: _Plugin) -> str: + """Return a canonical name for a plugin object. + + Note that a plugin may be registered under a different name + specified by the caller of :meth:`register(plugin, name) <register>`. + To obtain the name of a registered plugin use :meth:`get_name(plugin) + <get_name>` instead. + """ + name: str | None = getattr(plugin, "__name__", None) + return name or str(id(plugin)) + + def get_plugin(self, name: str) -> Any | None: + """Return the plugin registered under the given name, if any.""" return self._name2plugin.get(name) - def has_plugin(self, name): - """Return ``True`` if a plugin with the given name is registered.""" + def has_plugin(self, name: str) -> bool: + """Return whether a plugin with the given name is registered.""" return self.get_plugin(name) is not None - def get_name(self, plugin): - """Return name for registered plugin or ``None`` if not registered.""" + def get_name(self, plugin: _Plugin) -> str | None: + """Return the name the plugin is registered under, or ``None`` if + is isn't.""" for name, val in self._name2plugin.items(): if plugin == val: return name + return None - def _verify_hook(self, hook, hookimpl): - if hook.is_historic() and hookimpl.hookwrapper: + def _verify_hook(self, hook: HookCaller, hookimpl: HookImpl) -> None: + if hook.is_historic() and (hookimpl.hookwrapper or hookimpl.wrapper): raise PluginValidationError( hookimpl.plugin, - "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" + "Plugin %r\nhook %r\nhistoric incompatible with yield/wrapper/hookwrapper" % (hookimpl.plugin_name, hook.name), ) + assert hook.spec is not None if hook.spec.warn_on_impl: _warn_for_function(hook.spec.warn_on_impl, hookimpl.function) @@ -242,20 +353,38 @@ class PluginManager: ), ) - if hookimpl.hookwrapper and not inspect.isgeneratorfunction(hookimpl.function): + if hook.spec.warn_on_impl_args: + for hookimpl_argname in hookimpl.argnames: + argname_warning = hook.spec.warn_on_impl_args.get(hookimpl_argname) + if argname_warning is not None: + _warn_for_function(argname_warning, hookimpl.function) + + if ( + hookimpl.wrapper or hookimpl.hookwrapper + ) and not inspect.isgeneratorfunction(hookimpl.function): raise PluginValidationError( hookimpl.plugin, "Plugin %r for hook %r\nhookimpl definition: %s\n" - "Declared as hookwrapper=True but function is not a generator function" + "Declared as wrapper=True or hookwrapper=True " + "but function is not a generator function" % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)), ) - def check_pending(self): - """Verify that all hooks which have not been verified against - a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`.""" + if hookimpl.wrapper and hookimpl.hookwrapper: + raise PluginValidationError( + hookimpl.plugin, + "Plugin %r for hook %r\nhookimpl definition: %s\n" + "The wrapper=True and hookwrapper=True options are mutually exclusive" + % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)), + ) + + def check_pending(self) -> None: + """Verify that all hooks which have not been verified against a + hook specification are optional, otherwise raise + :exc:`PluginValidationError`.""" for name in self.hook.__dict__: if name[0] != "_": - hook = getattr(self.hook, name) + hook: HookCaller = getattr(self.hook, name) if not hook.has_spec(): for hookimpl in hook.get_hookimpls(): if not hookimpl.optionalhook: @@ -265,16 +394,21 @@ class PluginManager: % (name, hookimpl.plugin), ) - def load_setuptools_entrypoints(self, group, name=None): + def load_setuptools_entrypoints(self, group: str, name: str | None = None) -> int: """Load modules from querying the specified setuptools ``group``. - :param str group: entry point group to load plugins - :param str name: if given, loads only plugins with the given ``name``. - :rtype: int - :return: return the number of loaded plugins by this call. + :param group: + Entry point group to load plugins. + :param name: + If given, loads only plugins with the given ``name``. + + :return: + The number of plugins loaded by this call. """ + import importlib.metadata + count = 0 - for dist in list(importlib_metadata.distributions()): + for dist in list(importlib.metadata.distributions()): for ep in dist.entry_points: if ( ep.group != group @@ -290,84 +424,105 @@ class PluginManager: count += 1 return count - def list_plugin_distinfo(self): - """return list of distinfo/plugin tuples for all setuptools registered - plugins.""" + def list_plugin_distinfo(self) -> list[tuple[_Plugin, DistFacade]]: + """Return a list of (plugin, distinfo) pairs for all + setuptools-registered plugins.""" return list(self._plugin_distinfo) - def list_name_plugin(self): - """return list of name/plugin pairs.""" + def list_name_plugin(self) -> list[tuple[str, _Plugin]]: + """Return a list of (name, plugin) pairs for all registered plugins.""" return list(self._name2plugin.items()) - def get_hookcallers(self, plugin): - """get all hook callers for the specified plugin.""" - return self._plugin2hookcallers.get(plugin) + def get_hookcallers(self, plugin: _Plugin) -> list[HookCaller] | None: + """Get all hook callers for the specified plugin. - def add_hookcall_monitoring(self, before, after): - """add before/after tracing functions for all hooks - and return an undo function which, when called, - will remove the added tracers. + :returns: + The hook callers, or ``None`` if ``plugin`` is not registered in + this plugin manager. + """ + if self.get_name(plugin) is None: + return None + hookcallers = [] + for hookcaller in self.hook.__dict__.values(): + for hookimpl in hookcaller.get_hookimpls(): + if hookimpl.plugin is plugin: + hookcallers.append(hookcaller) + return hookcallers + + def add_hookcall_monitoring( + self, before: _BeforeTrace, after: _AfterTrace + ) -> Callable[[], None]: + """Add before/after tracing functions for all hooks. + + Returns an undo function which, when called, removes the added tracers. ``before(hook_name, hook_impls, kwargs)`` will be called ahead of all hook calls and receive a hookcaller instance, a list of HookImpl instances and the keyword arguments for the hook call. ``after(outcome, hook_name, hook_impls, kwargs)`` receives the - same arguments as ``before`` but also a :py:class:`pluggy._callers._Result` object + same arguments as ``before`` but also a :class:`~pluggy.Result` object which represents the result of the overall hook call. """ oldcall = self._inner_hookexec - def traced_hookexec(hook_name, hook_impls, kwargs, firstresult): - before(hook_name, hook_impls, kwargs) - outcome = _Result.from_call( - lambda: oldcall(hook_name, hook_impls, kwargs, firstresult) + def traced_hookexec( + hook_name: str, + hook_impls: Sequence[HookImpl], + caller_kwargs: Mapping[str, object], + firstresult: bool, + ) -> object | list[object]: + before(hook_name, hook_impls, caller_kwargs) + outcome = Result.from_call( + lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult) ) - after(outcome, hook_name, hook_impls, kwargs) + after(outcome, hook_name, hook_impls, caller_kwargs) return outcome.get_result() self._inner_hookexec = traced_hookexec - def undo(): + def undo() -> None: self._inner_hookexec = oldcall return undo - def enable_tracing(self): - """enable tracing of hook calls and return an undo function.""" + def enable_tracing(self) -> Callable[[], None]: + """Enable tracing of hook calls. + + Returns an undo function which, when called, removes the added tracing. + """ hooktrace = self.trace.root.get("hook") - def before(hook_name, methods, kwargs): + def before( + hook_name: str, methods: Sequence[HookImpl], kwargs: Mapping[str, object] + ) -> None: hooktrace.root.indent += 1 hooktrace(hook_name, kwargs) - def after(outcome, hook_name, methods, kwargs): - if outcome.excinfo is None: + def after( + outcome: Result[object], + hook_name: str, + methods: Sequence[HookImpl], + kwargs: Mapping[str, object], + ) -> None: + if outcome.exception is None: hooktrace("finish", hook_name, "-->", outcome.get_result()) hooktrace.root.indent -= 1 return self.add_hookcall_monitoring(before, after) - def subset_hook_caller(self, name, remove_plugins): - """Return a new :py:class:`._hooks._HookCaller` instance for the named method - which manages calls to all registered plugins except the - ones from remove_plugins.""" - orig = getattr(self.hook, name) - plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)] + def subset_hook_caller( + self, name: str, remove_plugins: Iterable[_Plugin] + ) -> HookCaller: + """Return a proxy :class:`~pluggy.HookCaller` instance for the named + method which manages calls to all registered plugins except the ones + from remove_plugins.""" + orig: HookCaller = getattr(self.hook, name) + plugins_to_remove = {plug for plug in remove_plugins if hasattr(plug, name)} if plugins_to_remove: - hc = _HookCaller( - orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts - ) - for hookimpl in orig.get_hookimpls(): - plugin = hookimpl.plugin - if plugin not in plugins_to_remove: - hc._add_hookimpl(hookimpl) - # we also keep track of this hook caller so it - # gets properly removed on plugin unregistration - self._plugin2hookcallers.setdefault(plugin, []).append(hc) - return hc + return _SubsetHookCaller(orig, plugins_to_remove) return orig -def _formatdef(func): +def _formatdef(func: Callable[..., object]) -> str: return f"{func.__name__}{inspect.signature(func)}" diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_result.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_result.py index 4c1f7f1f3c0..f9a081c4f68 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_result.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_result.py @@ -1,60 +1,104 @@ """ Hook wrapper "result" utilities. """ -import sys +from __future__ import annotations -def _raise_wrapfail(wrap_controller, msg): - co = wrap_controller.gi_code - raise RuntimeError( - "wrap_controller at %r %s:%d %s" - % (co.co_name, co.co_filename, co.co_firstlineno, msg) - ) +from types import TracebackType +from typing import Callable +from typing import cast +from typing import final +from typing import Generic +from typing import Optional +from typing import Tuple +from typing import Type +from typing import TypeVar + + +_ExcInfo = Tuple[Type[BaseException], BaseException, Optional[TracebackType]] +ResultType = TypeVar("ResultType") class HookCallError(Exception): - """Hook was called wrongly.""" + """Hook was called incorrectly.""" + + +@final +class Result(Generic[ResultType]): + """An object used to inspect and set the result in a :ref:`hook wrapper + <hookwrappers>`.""" + __slots__ = ("_result", "_exception") -class _Result: - def __init__(self, result, excinfo): + def __init__( + self, + result: ResultType | None, + exception: BaseException | None, + ) -> None: + """:meta private:""" self._result = result - self._excinfo = excinfo + self._exception = exception @property - def excinfo(self): - return self._excinfo + def excinfo(self) -> _ExcInfo | None: + """:meta private:""" + exc = self._exception + if exc is None: + return None + else: + return (type(exc), exc, exc.__traceback__) + + @property + def exception(self) -> BaseException | None: + """:meta private:""" + return self._exception @classmethod - def from_call(cls, func): + def from_call(cls, func: Callable[[], ResultType]) -> Result[ResultType]: + """:meta private:""" __tracebackhide__ = True - result = excinfo = None + result = exception = None try: result = func() - except BaseException: - excinfo = sys.exc_info() - - return cls(result, excinfo) + except BaseException as exc: + exception = exc + return cls(result, exception) - def force_result(self, result): + def force_result(self, result: ResultType) -> None: """Force the result(s) to ``result``. If the hook was marked as a ``firstresult`` a single value should - be set otherwise set a (modified) list of results. Any exceptions + be set, otherwise set a (modified) list of results. Any exceptions found during invocation will be deleted. + + This overrides any previous result or exception. """ self._result = result - self._excinfo = None + self._exception = None + + def force_exception(self, exception: BaseException) -> None: + """Force the result to fail with ``exception``. - def get_result(self): + This overrides any previous result or exception. + + .. versionadded:: 1.1.0 + """ + self._result = None + self._exception = exception + + def get_result(self) -> ResultType: """Get the result(s) for this hook call. If the hook was marked as a ``firstresult`` only a single value - will be returned otherwise a list of results. + will be returned, otherwise a list of results. """ __tracebackhide__ = True - if self._excinfo is None: - return self._result + exc = self._exception + if exc is None: + return cast(ResultType, self._result) else: - ex = self._excinfo - raise ex[1].with_traceback(ex[2]) + raise exc.with_traceback(exc.__traceback__) + + +# Historical name (pluggy<=1.2), kept for backward compatibility. +_Result = Result diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_tracing.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_tracing.py index 82c016271e1..cd238ad7e54 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_tracing.py +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_tracing.py @@ -2,17 +2,28 @@ Tracing utils """ +from __future__ import annotations + +from typing import Any +from typing import Callable +from typing import Sequence +from typing import Tuple + + +_Writer = Callable[[str], object] +_Processor = Callable[[Tuple[str, ...], Tuple[Any, ...]], object] + class TagTracer: - def __init__(self): - self._tags2proc = {} - self._writer = None + def __init__(self) -> None: + self._tags2proc: dict[tuple[str, ...], _Processor] = {} + self._writer: _Writer | None = None self.indent = 0 - def get(self, name): + def get(self, name: str) -> TagTracerSub: return TagTracerSub(self, (name,)) - def _format_message(self, tags, args): + def _format_message(self, tags: Sequence[str], args: Sequence[object]) -> str: if isinstance(args[-1], dict): extra = args[-1] args = args[:-1] @@ -29,7 +40,7 @@ class TagTracer: return "".join(lines) - def _processmessage(self, tags, args): + def _processmessage(self, tags: tuple[str, ...], args: tuple[object, ...]) -> None: if self._writer is not None and args: self._writer(self._format_message(tags, args)) try: @@ -39,10 +50,10 @@ class TagTracer: else: processor(tags, args) - def setwriter(self, writer): + def setwriter(self, writer: _Writer | None) -> None: self._writer = writer - def setprocessor(self, tags, processor): + def setprocessor(self, tags: str | tuple[str, ...], processor: _Processor) -> None: if isinstance(tags, str): tags = tuple(tags.split(":")) else: @@ -51,12 +62,12 @@ class TagTracer: class TagTracerSub: - def __init__(self, root, tags): + def __init__(self, root: TagTracer, tags: tuple[str, ...]) -> None: self.root = root self.tags = tags - def __call__(self, *args): + def __call__(self, *args: object) -> None: self.root._processmessage(self.tags, args) - def get(self, name): + def get(self, name: str) -> TagTracerSub: return self.__class__(self.root, self.tags + (name,)) diff --git a/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_warnings.py b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_warnings.py new file mode 100644 index 00000000000..6356c770c7d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/_warnings.py @@ -0,0 +1,27 @@ +from typing import final + + +class PluggyWarning(UserWarning): + """Base class for all warnings emitted by pluggy.""" + + __module__ = "pluggy" + + +@final +class PluggyTeardownRaisedWarning(PluggyWarning): + """A plugin raised an exception during an :ref:`old-style hookwrapper + <old_style_hookwrappers>` teardown. + + Such exceptions are not handled by pluggy, and may cause subsequent + teardowns to be executed at unexpected times, or be skipped entirely. + + This is an issue in the plugin implementation. + + If the exception is unintended, fix the underlying cause. + + If the exception is intended, switch to :ref:`new-style hook wrappers + <hookwrappers>`, or use :func:`result.force_exception() + <pluggy.Result.force_exception>` to set the exception instead of raising. + """ + + __module__ = "pluggy" diff --git a/tests/wpt/tests/tools/third_party/py/py/py.typed b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/py.typed index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/py/py/py.typed +++ b/tests/wpt/tests/tools/third_party/pluggy/src/pluggy/py.typed diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/benchmark.py b/tests/wpt/tests/tools/third_party/pluggy/testing/benchmark.py index b0d4b9536a0..d13e50aa465 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/benchmark.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/benchmark.py @@ -1,10 +1,14 @@ """ Benchmarking and performance tests. """ + import pytest -from pluggy import HookspecMarker, HookimplMarker, PluginManager -from pluggy._hooks import HookImpl + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager from pluggy._callers import _multicall +from pluggy._hooks import HookImpl hookspec = HookspecMarker("example") @@ -16,9 +20,9 @@ def hook(arg1, arg2, arg3): return arg1, arg2, arg3 -@hookimpl(hookwrapper=True) +@hookimpl(wrapper=True) def wrapper(arg1, arg2, arg3): - yield + return (yield) @pytest.fixture(params=[10, 100], ids="hooks={}".format) @@ -42,7 +46,7 @@ def test_hook_and_wrappers_speed(benchmark, hooks, wrappers): firstresult = False return (hook_name, hook_impls, caller_kwargs, firstresult), {} - benchmark.pedantic(_multicall, setup=setup) + benchmark.pedantic(_multicall, setup=setup, rounds=10) @pytest.mark.parametrize( @@ -67,30 +71,30 @@ def test_call_hook(benchmark, plugins, wrappers, nesting): class HookSpec: @hookspec def fun(self, hooks, nesting: int): - yield + pass class Plugin: - def __init__(self, num): + def __init__(self, num: int) -> None: self.num = num - def __repr__(self): + def __repr__(self) -> str: return f"<Plugin {self.num}>" @hookimpl - def fun(self, hooks, nesting: int): + def fun(self, hooks, nesting: int) -> None: if nesting: hooks.fun(hooks=hooks, nesting=nesting - 1) class PluginWrap: - def __init__(self, num): + def __init__(self, num: int) -> None: self.num = num - def __repr__(self): + def __repr__(self) -> str: return f"<PluginWrap {self.num}>" - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def fun(self): - yield + return (yield) pm.add_hookspecs(HookSpec) diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/conftest.py b/tests/wpt/tests/tools/third_party/pluggy/testing/conftest.py index 1fd4ecd5bdf..8842bd72644 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/conftest.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/conftest.py @@ -1,18 +1,19 @@ import pytest +from pluggy import HookspecMarker +from pluggy import PluginManager + @pytest.fixture( params=[lambda spec: spec, lambda spec: spec()], ids=["spec-is-class", "spec-is-instance"], ) -def he_pm(request, pm): - from pluggy import HookspecMarker - +def he_pm(request, pm: PluginManager) -> PluginManager: hookspec = HookspecMarker("example") class Hooks: @hookspec - def he_method1(self, arg): + def he_method1(self, arg: int) -> int: return arg + 1 pm.add_hookspecs(request.param(Hooks)) @@ -20,7 +21,5 @@ def he_pm(request, pm): @pytest.fixture -def pm(): - from pluggy import PluginManager - +def pm() -> PluginManager: return PluginManager("example") diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_details.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_details.py index 0ceb3b3eb13..9b68a0814f8 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_details.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_details.py @@ -1,18 +1,21 @@ -import warnings import pytest -from pluggy import PluginManager, HookimplMarker, HookspecMarker + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager + hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") -def test_parse_hookimpl_override(): +def test_parse_hookimpl_override() -> None: class MyPluginManager(PluginManager): def parse_hookimpl_opts(self, module_or_class, name): opts = PluginManager.parse_hookimpl_opts(self, module_or_class, name) if opts is None: if name.startswith("x1"): - opts = {} + opts = {} # type: ignore[assignment] return opts class Plugin: @@ -23,6 +26,10 @@ def test_parse_hookimpl_override(): def x1meth2(self): yield # pragma: no cover + @hookimpl(wrapper=True, trylast=True) + def x1meth3(self): + return (yield) # pragma: no cover + class Spec: @hookspec def x1meth(self): @@ -32,19 +39,37 @@ def test_parse_hookimpl_override(): def x1meth2(self): pass + @hookspec + def x1meth3(self): + pass + pm = MyPluginManager(hookspec.project_name) pm.register(Plugin()) pm.add_hookspecs(Spec) - assert not pm.hook.x1meth._nonwrappers[0].hookwrapper - assert not pm.hook.x1meth._nonwrappers[0].tryfirst - assert not pm.hook.x1meth._nonwrappers[0].trylast - assert not pm.hook.x1meth._nonwrappers[0].optionalhook - - assert pm.hook.x1meth2._wrappers[0].tryfirst - assert pm.hook.x1meth2._wrappers[0].hookwrapper - -def test_warn_when_deprecated_specified(recwarn): + hookimpls = pm.hook.x1meth.get_hookimpls() + assert len(hookimpls) == 1 + assert not hookimpls[0].hookwrapper + assert not hookimpls[0].wrapper + assert not hookimpls[0].tryfirst + assert not hookimpls[0].trylast + assert not hookimpls[0].optionalhook + + hookimpls = pm.hook.x1meth2.get_hookimpls() + assert len(hookimpls) == 1 + assert hookimpls[0].hookwrapper + assert not hookimpls[0].wrapper + assert hookimpls[0].tryfirst + + hookimpls = pm.hook.x1meth3.get_hookimpls() + assert len(hookimpls) == 1 + assert not hookimpls[0].hookwrapper + assert hookimpls[0].wrapper + assert not hookimpls[0].tryfirst + assert hookimpls[0].trylast + + +def test_warn_when_deprecated_specified(recwarn) -> None: warning = DeprecationWarning("foo is deprecated") class Spec: @@ -68,20 +93,53 @@ def test_warn_when_deprecated_specified(recwarn): assert record.lineno == Plugin.foo.__code__.co_firstlineno -def test_plugin_getattr_raises_errors(): +def test_warn_when_deprecated_args_specified(recwarn) -> None: + warning1 = DeprecationWarning("old1 is deprecated") + warning2 = DeprecationWarning("old2 is deprecated") + + class Spec: + @hookspec( + warn_on_impl_args={ + "old1": warning1, + "old2": warning2, + }, + ) + def foo(self, old1, new, old2): + raise NotImplementedError() + + class Plugin: + @hookimpl + def foo(self, old2, old1, new): + raise NotImplementedError() + + pm = PluginManager(hookspec.project_name) + pm.add_hookspecs(Spec) + + with pytest.warns(DeprecationWarning) as records: + pm.register(Plugin()) + (record1, record2) = records + assert record1.message is warning2 + assert record1.filename == Plugin.foo.__code__.co_filename + assert record1.lineno == Plugin.foo.__code__.co_firstlineno + assert record2.message is warning1 + assert record2.filename == Plugin.foo.__code__.co_filename + assert record2.lineno == Plugin.foo.__code__.co_firstlineno + + +def test_plugin_getattr_raises_errors() -> None: """Pluggy must be able to handle plugins which raise weird exceptions when getattr() gets called (#11). """ class DontTouchMe: def __getattr__(self, x): - raise Exception("cant touch me") + raise Exception("can't touch me") class Module: pass module = Module() - module.x = DontTouchMe() + module.x = DontTouchMe() # type: ignore[attr-defined] pm = PluginManager(hookspec.project_name) # register() would raise an error @@ -89,38 +147,36 @@ def test_plugin_getattr_raises_errors(): assert pm.get_plugin("donttouch") is module -def test_warning_on_call_vs_hookspec_arg_mismatch(): - """Verify that is a hook is called with less arguments then defined in the - spec that a warning is emitted. - """ +def test_not_all_arguments_are_provided_issues_a_warning(pm: PluginManager) -> None: + """Calling a hook without providing all arguments specified in + the hook spec issues a warning.""" class Spec: @hookspec - def myhook(self, arg1, arg2): + def hello(self, arg1, arg2): pass - class Plugin: - @hookimpl - def myhook(self, arg1): + @hookspec(historic=True) + def herstory(self, arg1, arg2): pass - pm = PluginManager(hookspec.project_name) - pm.register(Plugin()) - pm.add_hookspecs(Spec()) + pm.add_hookspecs(Spec) - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") + with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"): + pm.hook.hello() + with pytest.warns(UserWarning, match=r"'arg2'.*cannot be found.*$"): + pm.hook.hello(arg1=1) + with pytest.warns(UserWarning, match=r"'arg1'.*cannot be found.*$"): + pm.hook.hello(arg2=2) - # calling should trigger a warning - pm.hook.myhook(arg1=1) + with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"): + pm.hook.hello.call_extra([], kwargs=dict()) - assert len(warns) == 1 - warning = warns[-1] - assert issubclass(warning.category, Warning) - assert "Argument(s) ('arg2',)" in str(warning.message) + with pytest.warns(UserWarning, match=r"'arg1', 'arg2'.*cannot be found.*$"): + pm.hook.herstory.call_historic(kwargs=dict()) -def test_repr(): +def test_repr() -> None: class Plugin: @hookimpl def myhook(self): @@ -130,6 +186,6 @@ def test_repr(): plugin = Plugin() pname = pm.register(plugin) - assert repr(pm.hook.myhook._nonwrappers[0]) == ( + assert repr(pm.hook.myhook.get_hookimpls()[0]) == ( f"<HookImpl plugin_name={pname!r}, plugin={plugin!r}>" ) diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_helpers.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_helpers.py index 465858c499c..4fe26a57bc5 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_helpers.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_helpers.py @@ -1,17 +1,23 @@ +from functools import wraps +from typing import Any +from typing import Callable +from typing import cast +from typing import TypeVar + from pluggy._hooks import varnames from pluggy._manager import _formatdef -def test_varnames(): - def f(x): +def test_varnames() -> None: + def f(x) -> None: i = 3 # noqa class A: - def f(self, y): + def f(self, y) -> None: pass class B: - def __call__(self, z): + def __call__(self, z) -> None: pass assert varnames(f) == (("x",), ()) @@ -19,23 +25,23 @@ def test_varnames(): assert varnames(B()) == (("z",), ()) -def test_varnames_default(): - def f(x, y=3): +def test_varnames_default() -> None: + def f(x, y=3) -> None: pass assert varnames(f) == (("x",), ("y",)) -def test_varnames_class(): +def test_varnames_class() -> None: class C: - def __init__(self, x): + def __init__(self, x) -> None: pass class D: pass class E: - def __init__(self, x): + def __init__(self, x) -> None: pass class F: @@ -47,14 +53,14 @@ def test_varnames_class(): assert varnames(F) == ((), ()) -def test_varnames_keyword_only(): - def f1(x, *, y): +def test_varnames_keyword_only() -> None: + def f1(x, *, y) -> None: pass - def f2(x, *, y=3): + def f2(x, *, y=3) -> None: pass - def f3(x=1, *, y=3): + def f3(x=1, *, y=3) -> None: pass assert varnames(f1) == (("x",), ()) @@ -62,7 +68,7 @@ def test_varnames_keyword_only(): assert varnames(f3) == ((), ("x",)) -def test_formatdef(): +def test_formatdef() -> None: def function1(): pass @@ -82,3 +88,29 @@ def test_formatdef(): pass assert _formatdef(function4) == "function4(arg1, *args, **kwargs)" + + +def test_varnames_decorator() -> None: + F = TypeVar("F", bound=Callable[..., Any]) + + def my_decorator(func: F) -> F: + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return cast(F, wrapper) + + @my_decorator + def example(a, b=123) -> None: + pass + + class Example: + @my_decorator + def example_method(self, x, y=1) -> None: + pass + + ex_inst = Example() + + assert varnames(example) == (("a",), ("b",)) + assert varnames(Example.example_method) == (("x",), ("y",)) + assert varnames(ex_inst.example_method) == (("x",), ("y",)) diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_hookcaller.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_hookcaller.py index 9eeaef86666..3db76de2562 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_hookcaller.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_hookcaller.py @@ -1,90 +1,122 @@ +from typing import Callable +from typing import Generator +from typing import List +from typing import Sequence +from typing import TypeVar + import pytest -from pluggy import HookimplMarker, HookspecMarker, PluginValidationError +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager +from pluggy import PluginValidationError +from pluggy._hooks import HookCaller from pluggy._hooks import HookImpl + hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") @pytest.fixture -def hc(pm): +def hc(pm: PluginManager) -> HookCaller: class Hooks: @hookspec - def he_method1(self, arg): + def he_method1(self, arg: object) -> None: pass pm.add_hookspecs(Hooks) return pm.hook.he_method1 -@pytest.fixture -def addmeth(hc): - def addmeth(tryfirst=False, trylast=False, hookwrapper=False): - def wrap(func): - hookimpl(tryfirst=tryfirst, trylast=trylast, hookwrapper=hookwrapper)(func) - hc._add_hookimpl(HookImpl(None, "<temp>", func, func.example_impl)) +FuncT = TypeVar("FuncT", bound=Callable[..., object]) + + +class AddMeth: + def __init__(self, hc: HookCaller) -> None: + self.hc = hc + + def __call__( + self, + tryfirst: bool = False, + trylast: bool = False, + hookwrapper: bool = False, + wrapper: bool = False, + ) -> Callable[[FuncT], FuncT]: + def wrap(func: FuncT) -> FuncT: + hookimpl( + tryfirst=tryfirst, + trylast=trylast, + hookwrapper=hookwrapper, + wrapper=wrapper, + )(func) + self.hc._add_hookimpl( + HookImpl(None, "<temp>", func, func.example_impl), # type: ignore[attr-defined] + ) return func return wrap - return addmeth + +@pytest.fixture +def addmeth(hc: HookCaller) -> AddMeth: + return AddMeth(hc) -def funcs(hookmethods): +def funcs(hookmethods: Sequence[HookImpl]) -> List[Callable[..., object]]: return [hookmethod.function for hookmethod in hookmethods] -def test_adding_nonwrappers(hc, addmeth): +def test_adding_nonwrappers(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() - def he_method1(): + def he_method1() -> None: pass @addmeth() - def he_method2(): + def he_method2() -> None: pass @addmeth() - def he_method3(): + def he_method3() -> None: pass - assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3] + assert funcs(hc.get_hookimpls()) == [he_method1, he_method2, he_method3] -def test_adding_nonwrappers_trylast(hc, addmeth): +def test_adding_nonwrappers_trylast(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() - def he_method1_middle(): + def he_method1_middle() -> None: pass @addmeth(trylast=True) - def he_method1(): + def he_method1() -> None: pass @addmeth() - def he_method1_b(): + def he_method1_b() -> None: pass - assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b] + assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b] -def test_adding_nonwrappers_trylast3(hc, addmeth): +def test_adding_nonwrappers_trylast3(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() - def he_method1_a(): + def he_method1_a() -> None: pass @addmeth(trylast=True) - def he_method1_b(): + def he_method1_b() -> None: pass @addmeth() - def he_method1_c(): + def he_method1_c() -> None: pass @addmeth(trylast=True) - def he_method1_d(): + def he_method1_d() -> None: pass - assert funcs(hc._nonwrappers) == [ + assert funcs(hc.get_hookimpls()) == [ he_method1_d, he_method1_b, he_method1_a, @@ -92,93 +124,212 @@ def test_adding_nonwrappers_trylast3(hc, addmeth): ] -def test_adding_nonwrappers_trylast2(hc, addmeth): +def test_adding_nonwrappers_trylast2(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth() - def he_method1_middle(): + def he_method1_middle() -> None: pass @addmeth() - def he_method1_b(): + def he_method1_b() -> None: pass @addmeth(trylast=True) - def he_method1(): + def he_method1() -> None: pass - assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b] + assert funcs(hc.get_hookimpls()) == [he_method1, he_method1_middle, he_method1_b] -def test_adding_nonwrappers_tryfirst(hc, addmeth): +def test_adding_nonwrappers_tryfirst(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(tryfirst=True) - def he_method1(): + def he_method1() -> None: pass @addmeth() - def he_method1_middle(): + def he_method1_middle() -> None: pass @addmeth() - def he_method1_b(): + def he_method1_b() -> None: pass - assert funcs(hc._nonwrappers) == [he_method1_middle, he_method1_b, he_method1] + assert funcs(hc.get_hookimpls()) == [he_method1_middle, he_method1_b, he_method1] -def test_adding_wrappers_ordering(hc, addmeth): +def test_adding_wrappers_ordering(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(hookwrapper=True) def he_method1(): - pass + yield + + @addmeth(wrapper=True) + def he_method1_fun(): + yield @addmeth() def he_method1_middle(): - pass + return @addmeth(hookwrapper=True) - def he_method3(): - pass + def he_method3_fun(): + yield - assert funcs(hc._nonwrappers) == [he_method1_middle] - assert funcs(hc._wrappers) == [he_method1, he_method3] + @addmeth(hookwrapper=True) + def he_method3(): + yield + + assert funcs(hc.get_hookimpls()) == [ + he_method1_middle, + he_method1, + he_method1_fun, + he_method3_fun, + he_method3, + ] -def test_adding_wrappers_ordering_tryfirst(hc, addmeth): +def test_adding_wrappers_ordering_tryfirst(hc: HookCaller, addmeth: AddMeth) -> None: @addmeth(hookwrapper=True, tryfirst=True) def he_method1(): - pass + yield @addmeth(hookwrapper=True) def he_method2(): - pass + yield + + @addmeth(wrapper=True, tryfirst=True) + def he_method3(): + yield + + assert funcs(hc.get_hookimpls()) == [he_method2, he_method1, he_method3] + + +def test_adding_wrappers_complex(hc: HookCaller, addmeth: AddMeth) -> None: + assert funcs(hc.get_hookimpls()) == [] + + @addmeth(hookwrapper=True, trylast=True) + def m1(): + yield + + assert funcs(hc.get_hookimpls()) == [m1] + + @addmeth() + def m2() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m2, m1] + + @addmeth(trylast=True) + def m3() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m3, m2, m1] + + @addmeth(hookwrapper=True) + def m4() -> None: ... - assert hc._nonwrappers == [] - assert funcs(hc._wrappers) == [he_method2, he_method1] + assert funcs(hc.get_hookimpls()) == [m3, m2, m1, m4] + @addmeth(wrapper=True, tryfirst=True) + def m5(): + yield -def test_hookspec(pm): + assert funcs(hc.get_hookimpls()) == [m3, m2, m1, m4, m5] + + @addmeth(tryfirst=True) + def m6() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m3, m2, m6, m1, m4, m5] + + @addmeth() + def m7() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m3, m2, m7, m6, m1, m4, m5] + + @addmeth(wrapper=True) + def m8(): + yield + + assert funcs(hc.get_hookimpls()) == [m3, m2, m7, m6, m1, m4, m8, m5] + + @addmeth(trylast=True) + def m9() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m1, m4, m8, m5] + + @addmeth(tryfirst=True) + def m10() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m10, m1, m4, m8, m5] + + @addmeth(hookwrapper=True, trylast=True) + def m11() -> None: ... + + assert funcs(hc.get_hookimpls()) == [m9, m3, m2, m7, m6, m10, m11, m1, m4, m8, m5] + + @addmeth(wrapper=True) + def m12(): + yield + + assert funcs(hc.get_hookimpls()) == [ + m9, + m3, + m2, + m7, + m6, + m10, + m11, + m1, + m4, + m8, + m12, + m5, + ] + + @addmeth() + def m13() -> None: ... + + assert funcs(hc.get_hookimpls()) == [ + m9, + m3, + m2, + m7, + m13, + m6, + m10, + m11, + m1, + m4, + m8, + m12, + m5, + ] + + +def test_hookspec(pm: PluginManager) -> None: class HookSpec: @hookspec() - def he_myhook1(arg1): + def he_myhook1(arg1) -> None: pass @hookspec(firstresult=True) - def he_myhook2(arg1): + def he_myhook2(arg1) -> None: pass @hookspec(firstresult=False) - def he_myhook3(arg1): + def he_myhook3(arg1) -> None: pass pm.add_hookspecs(HookSpec) + assert pm.hook.he_myhook1.spec is not None assert not pm.hook.he_myhook1.spec.opts["firstresult"] + assert pm.hook.he_myhook2.spec is not None assert pm.hook.he_myhook2.spec.opts["firstresult"] + assert pm.hook.he_myhook3.spec is not None assert not pm.hook.he_myhook3.spec.opts["firstresult"] @pytest.mark.parametrize("name", ["hookwrapper", "optionalhook", "tryfirst", "trylast"]) @pytest.mark.parametrize("val", [True, False]) -def test_hookimpl(name, val): - @hookimpl(**{name: val}) - def he_myhook1(arg1): +def test_hookimpl(name: str, val: bool) -> None: + @hookimpl(**{name: val}) # type: ignore[misc,call-overload] + def he_myhook1(arg1) -> None: pass if val: @@ -187,13 +338,13 @@ def test_hookimpl(name, val): assert not hasattr(he_myhook1, name) -def test_hookrelay_registry(pm): +def test_hookrelay_registry(pm: PluginManager) -> None: """Verify hook caller instances are registered by name onto the relay and can be likewise unregistered.""" class Api: @hookspec - def hello(self, arg): + def hello(self, arg: object) -> None: "api hook 1" pm.add_hookspecs(Api) @@ -215,13 +366,13 @@ def test_hookrelay_registry(pm): assert hook.hello(arg=3) == [] -def test_hookrelay_registration_by_specname(pm): +def test_hookrelay_registration_by_specname(pm: PluginManager) -> None: """Verify hook caller instances may also be registered by specifying a specname option to the hookimpl""" class Api: @hookspec - def hello(self, arg): + def hello(self, arg: object) -> None: "api hook 1" pm.add_hookspecs(Api) @@ -231,7 +382,7 @@ def test_hookrelay_registration_by_specname(pm): class Plugin: @hookimpl(specname="hello") - def foo(self, arg): + def foo(self, arg: int) -> int: return arg + 1 plugin = Plugin() @@ -240,13 +391,13 @@ def test_hookrelay_registration_by_specname(pm): assert out == [4] -def test_hookrelay_registration_by_specname_raises(pm): +def test_hookrelay_registration_by_specname_raises(pm: PluginManager) -> None: """Verify using specname still raises the types of errors during registration as it would have without using specname.""" class Api: @hookspec - def hello(self, arg): + def hello(self, arg: object) -> None: "api hook 1" pm.add_hookspecs(Api) @@ -254,7 +405,7 @@ def test_hookrelay_registration_by_specname_raises(pm): # make sure a bad signature still raises an error when using specname class Plugin: @hookimpl(specname="hello") - def foo(self, arg, too, many, args): + def foo(self, arg: int, too, many, args) -> int: return arg + 1 with pytest.raises(PluginValidationError): @@ -264,9 +415,100 @@ def test_hookrelay_registration_by_specname_raises(pm): # corresponding spec. EVEN if the function name matches one. class Plugin2: @hookimpl(specname="bar") - def hello(self, arg): + def hello(self, arg: int) -> int: return arg + 1 pm.register(Plugin2()) with pytest.raises(PluginValidationError): pm.check_pending() + + +def test_hook_conflict(pm: PluginManager) -> None: + class Api1: + @hookspec + def conflict(self) -> None: + """Api1 hook""" + + class Api2: + @hookspec + def conflict(self) -> None: + """Api2 hook""" + + pm.add_hookspecs(Api1) + with pytest.raises(ValueError) as exc: + pm.add_hookspecs(Api2) + assert str(exc.value) == ( + "Hook 'conflict' is already registered within namespace " + "<class 'test_hookcaller.test_hook_conflict.<locals>.Api1'>" + ) + + +def test_call_extra_hook_order(hc: HookCaller, addmeth: AddMeth) -> None: + """Ensure that call_extra is calling hooks in the right order.""" + order = [] + + @addmeth(tryfirst=True) + def method1() -> str: + order.append("1") + return "1" + + @addmeth() + def method2() -> str: + order.append("2") + return "2" + + @addmeth(trylast=True) + def method3() -> str: + order.append("3") + return "3" + + @addmeth(wrapper=True, tryfirst=True) + def method4() -> Generator[None, str, str]: + order.append("4pre") + result = yield + order.append("4post") + return result + + @addmeth(wrapper=True) + def method5() -> Generator[None, str, str]: + order.append("5pre") + result = yield + order.append("5post") + return result + + @addmeth(wrapper=True, trylast=True) + def method6() -> Generator[None, str, str]: + order.append("6pre") + result = yield + order.append("6post") + return result + + def extra1() -> str: + order.append("extra1") + return "extra1" + + def extra2() -> str: + order.append("extra2") + return "extra2" + + result = hc.call_extra([extra1, extra2], {"arg": "test"}) + assert order == [ + "4pre", + "5pre", + "6pre", + "1", + "extra2", + "extra1", + "2", + "3", + "6post", + "5post", + "4post", + ] + assert result == [ + "1", + "extra2", + "extra1", + "2", + "3", + ] diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_invocations.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_invocations.py index 323b9b21e8b..833ec2b79ce 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_invocations.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_invocations.py @@ -1,12 +1,18 @@ +from typing import Iterator + import pytest -from pluggy import PluginValidationError, HookimplMarker, HookspecMarker + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager +from pluggy import PluginValidationError hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") -def test_argmismatch(pm): +def test_argmismatch(pm: PluginManager) -> None: class Api: @hookspec def hello(self, arg): @@ -25,7 +31,7 @@ def test_argmismatch(pm): assert "argwrong" in str(exc.value) -def test_only_kwargs(pm): +def test_only_kwargs(pm: PluginManager) -> None: class Api: @hookspec def hello(self, arg): @@ -33,14 +39,14 @@ def test_only_kwargs(pm): pm.add_hookspecs(Api) with pytest.raises(TypeError) as exc: - pm.hook.hello(3) + pm.hook.hello(3) # type: ignore[call-arg] - comprehensible = "hook calling supports only keyword arguments" - assert comprehensible in str(exc.value) + message = "__call__() takes 1 positional argument but 2 were given" + assert message in str(exc.value) -def test_opt_in_args(pm): - """Verfiy that two hookimpls with mutex args can serve +def test_opt_in_args(pm: PluginManager) -> None: + """Verify that two hookimpls with mutex args can serve under the same spec. """ @@ -67,7 +73,7 @@ def test_opt_in_args(pm): assert results == [2, 1] -def test_call_order(pm): +def test_call_order(pm: PluginManager) -> None: class Api: @hookspec def hello(self, arg): @@ -96,16 +102,27 @@ def test_call_order(pm): assert arg == 0 outcome = yield assert outcome.get_result() == [3, 2, 1] + assert outcome.exception is None + assert outcome.excinfo is None + + class Plugin5: + @hookimpl(wrapper=True) + def hello(self, arg): + assert arg == 0 + result = yield + assert result == [3, 2, 1] + return result pm.register(Plugin1()) pm.register(Plugin2()) pm.register(Plugin3()) pm.register(Plugin4()) # hookwrapper should get same list result + pm.register(Plugin5()) # hookwrapper should get same list result res = pm.hook.hello(arg=0) assert res == [3, 2, 1] -def test_firstresult_definition(pm): +def test_firstresult_definition(pm: PluginManager) -> None: class Api: @hookspec(firstresult=True) def hello(self, arg): @@ -129,6 +146,14 @@ def test_firstresult_definition(pm): return None class Plugin4: + @hookimpl(wrapper=True) + def hello(self, arg): + assert arg == 3 + outcome = yield + assert outcome == 2 + return outcome + + class Plugin5: @hookimpl(hookwrapper=True) def hello(self, arg): assert arg == 3 @@ -138,12 +163,13 @@ def test_firstresult_definition(pm): pm.register(Plugin1()) # discarded - not the last registered plugin pm.register(Plugin2()) # used as result pm.register(Plugin3()) # None result is ignored - pm.register(Plugin4()) # hookwrapper should get same non-list result + pm.register(Plugin4()) # wrapper should get same non-list result + pm.register(Plugin5()) # hookwrapper should get same non-list result res = pm.hook.hello(arg=3) assert res == 2 -def test_firstresult_force_result(pm): +def test_firstresult_force_result_hookwrapper(pm: PluginManager) -> None: """Verify forcing a result in a wrapper.""" class Api: @@ -178,7 +204,42 @@ def test_firstresult_force_result(pm): assert res == 0 # this result is forced and not a list -def test_firstresult_returns_none(pm): +def test_firstresult_force_result(pm: PluginManager) -> None: + """Verify forcing a result in a wrapper.""" + + class Api: + @hookspec(firstresult=True) + def hello(self, arg): + "api hook 1" + + pm.add_hookspecs(Api) + + class Plugin1: + @hookimpl + def hello(self, arg): + return arg + 1 + + class Plugin2: + @hookimpl(wrapper=True) + def hello(self, arg): + assert arg == 3 + outcome = yield + assert outcome == 4 + return 0 + + class Plugin3: + @hookimpl + def hello(self, arg): + return None + + pm.register(Plugin1()) + pm.register(Plugin2()) # wrapper + pm.register(Plugin3()) # ignored since returns None + res = pm.hook.hello(arg=3) + assert res == 0 # this result is forced and not a list + + +def test_firstresult_returns_none(pm: PluginManager) -> None: """If None results are returned by underlying implementations ensure the multi-call loop returns a None value. """ @@ -200,7 +261,7 @@ def test_firstresult_returns_none(pm): assert res is None -def test_firstresult_no_plugin(pm): +def test_firstresult_no_plugin(pm: PluginManager) -> None: """If no implementations/plugins have been registered for a firstresult hook the multi-call loop should return a None value. """ @@ -213,3 +274,55 @@ def test_firstresult_no_plugin(pm): pm.add_hookspecs(Api) res = pm.hook.hello(arg=3) assert res is None + + +def test_no_hookspec(pm: PluginManager) -> None: + """A hook with hookimpls can still be called even if no hookspec + was registered for it (and call_pending wasn't called to check + against it). + """ + + class Plugin: + @hookimpl + def hello(self, arg): + return "Plugin.hello" + + pm.register(Plugin()) + + assert pm.hook.hello(arg=10, extra=20) == ["Plugin.hello"] + + +def test_non_wrapper_generator(pm: PluginManager) -> None: + """A hookimpl can be a generator without being a wrapper, + meaning it returns an iterator result.""" + + class Api: + @hookspec + def hello(self) -> Iterator[int]: + raise NotImplementedError() + + pm.add_hookspecs(Api) + + class Plugin1: + @hookimpl + def hello(self): + yield 1 + + class Plugin2: + @hookimpl + def hello(self): + yield 2 + yield 3 + + class Plugin3: + @hookimpl(wrapper=True) + def hello(self): + return (yield) + + pm.register(Plugin1()) + pm.register(Plugin2()) # wrapper + res = pm.hook.hello() + assert [y for x in res for y in x] == [2, 3, 1] + pm.register(Plugin3()) + res = pm.hook.hello() + assert [y for x in res for y in x] == [2, 3, 1] diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_multicall.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_multicall.py index 8ffb452f694..7d8d8f2881a 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_multicall.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_multicall.py @@ -1,23 +1,37 @@ +from typing import Callable +from typing import List +from typing import Mapping +from typing import Sequence +from typing import Type +from typing import Union + import pytest -from pluggy import HookCallError, HookspecMarker, HookimplMarker -from pluggy._hooks import HookImpl + +from pluggy import HookCallError +from pluggy import HookimplMarker +from pluggy import HookspecMarker from pluggy._callers import _multicall +from pluggy._hooks import HookImpl hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") -def MC(methods, kwargs, firstresult=False): +def MC( + methods: Sequence[Callable[..., object]], + kwargs: Mapping[str, object], + firstresult: bool = False, +) -> Union[object, List[object]]: caller = _multicall hookfuncs = [] for method in methods: - f = HookImpl(None, "<temp>", method, method.example_impl) + f = HookImpl(None, "<temp>", method, method.example_impl) # type: ignore[attr-defined] hookfuncs.append(f) return caller("foo", hookfuncs, kwargs, firstresult) -def test_keyword_args(): +def test_keyword_args() -> None: @hookimpl def f(x): return x + 1 @@ -31,7 +45,7 @@ def test_keyword_args(): assert reslist == [24 + 23, 24] -def test_keyword_args_with_defaultargs(): +def test_keyword_args_with_defaultargs() -> None: @hookimpl def f(x, z=1): return x + z @@ -40,7 +54,7 @@ def test_keyword_args_with_defaultargs(): assert reslist == [24] -def test_tags_call_error(): +def test_tags_call_error() -> None: @hookimpl def f(x): return x @@ -49,7 +63,7 @@ def test_tags_call_error(): MC([f], {}) -def test_call_none_is_no_result(): +def test_call_none_is_no_result() -> None: @hookimpl def m1(): return 1 @@ -60,11 +74,11 @@ def test_call_none_is_no_result(): res = MC([m1, m2], {}, firstresult=True) assert res == 1 - res = MC([m1, m2], {}, {}) + res = MC([m1, m2], {}, firstresult=False) assert res == [1] -def test_hookwrapper(): +def test_hookwrapper() -> None: out = [] @hookimpl(hookwrapper=True) @@ -87,7 +101,51 @@ def test_hookwrapper(): assert out == ["m1 init", "m2", "m1 finish"] -def test_hookwrapper_order(): +def test_hookwrapper_two_yields() -> None: + @hookimpl(hookwrapper=True) + def m(): + yield + yield + + with pytest.raises(RuntimeError, match="has second yield"): + MC([m], {}) + + +def test_wrapper() -> None: + out = [] + + @hookimpl(wrapper=True) + def m1(): + out.append("m1 init") + result = yield + out.append("m1 finish") + return result * 2 + + @hookimpl + def m2(): + out.append("m2") + return 2 + + res = MC([m2, m1], {}) + assert res == [2, 2] + assert out == ["m1 init", "m2", "m1 finish"] + out[:] = [] + res = MC([m2, m1], {}, firstresult=True) + assert res == 4 + assert out == ["m1 init", "m2", "m1 finish"] + + +def test_wrapper_two_yields() -> None: + @hookimpl(wrapper=True) + def m(): + yield + yield + + with pytest.raises(RuntimeError, match="has second yield"): + MC([m], {}) + + +def test_hookwrapper_order() -> None: out = [] @hookimpl(hookwrapper=True) @@ -96,18 +154,40 @@ def test_hookwrapper_order(): yield 1 out.append("m1 finish") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def m2(): out.append("m2 init") - yield 2 + result = yield 2 out.append("m2 finish") + return result - res = MC([m2, m1], {}) - assert res == [] - assert out == ["m1 init", "m2 init", "m2 finish", "m1 finish"] + @hookimpl(hookwrapper=True) + def m3(): + out.append("m3 init") + yield 3 + out.append("m3 finish") + @hookimpl(hookwrapper=True) + def m4(): + out.append("m4 init") + yield 4 + out.append("m4 finish") -def test_hookwrapper_not_yield(): + res = MC([m4, m3, m2, m1], {}) + assert res == [] + assert out == [ + "m1 init", + "m2 init", + "m3 init", + "m4 init", + "m4 finish", + "m3 finish", + "m2 finish", + "m1 finish", + ] + + +def test_hookwrapper_not_yield() -> None: @hookimpl(hookwrapper=True) def m1(): pass @@ -116,7 +196,17 @@ def test_hookwrapper_not_yield(): MC([m1], {}) -def test_hookwrapper_too_many_yield(): +def test_hookwrapper_yield_not_executed() -> None: + @hookimpl(hookwrapper=True) + def m1(): + if False: + yield # type: ignore[unreachable] + + with pytest.raises(RuntimeError, match="did not yield"): + MC([m1], {}) + + +def test_hookwrapper_too_many_yield() -> None: @hookimpl(hookwrapper=True) def m1(): yield 1 @@ -128,14 +218,47 @@ def test_hookwrapper_too_many_yield(): assert (__file__ + ":") in str(ex.value) +def test_wrapper_yield_not_executed() -> None: + @hookimpl(wrapper=True) + def m1(): + if False: + yield # type: ignore[unreachable] + + with pytest.raises(RuntimeError, match="did not yield"): + MC([m1], {}) + + +def test_wrapper_too_many_yield() -> None: + out = [] + + @hookimpl(wrapper=True) + def m1(): + try: + yield 1 + yield 2 + finally: + out.append("cleanup") + + with pytest.raises(RuntimeError) as ex: + try: + MC([m1], {}) + finally: + out.append("finally") + assert "m1" in str(ex.value) + assert (__file__ + ":") in str(ex.value) + assert out == ["cleanup", "finally"] + + @pytest.mark.parametrize("exc", [ValueError, SystemExit]) -def test_hookwrapper_exception(exc): +def test_hookwrapper_exception(exc: "Type[BaseException]") -> None: out = [] @hookimpl(hookwrapper=True) def m1(): out.append("m1 init") - yield None + result = yield + assert isinstance(result.exception, exc) + assert result.excinfo[0] == exc out.append("m1 finish") @hookimpl @@ -145,3 +268,187 @@ def test_hookwrapper_exception(exc): with pytest.raises(exc): MC([m2, m1], {}) assert out == ["m1 init", "m1 finish"] + + +def test_hookwrapper_force_exception() -> None: + out = [] + + @hookimpl(hookwrapper=True) + def m1(): + out.append("m1 init") + result = yield + try: + result.get_result() + except BaseException as exc: + result.force_exception(exc) + out.append("m1 finish") + + @hookimpl(hookwrapper=True) + def m2(): + out.append("m2 init") + result = yield + try: + result.get_result() + except BaseException as exc: + new_exc = OSError("m2") + new_exc.__cause__ = exc + result.force_exception(new_exc) + out.append("m2 finish") + + @hookimpl(hookwrapper=True) + def m3(): + out.append("m3 init") + yield + out.append("m3 finish") + + @hookimpl + def m4(): + raise ValueError("m4") + + with pytest.raises(OSError, match="m2") as excinfo: + MC([m4, m3, m2, m1], {}) + assert out == [ + "m1 init", + "m2 init", + "m3 init", + "m3 finish", + "m2 finish", + "m1 finish", + ] + assert excinfo.value.__cause__ is not None + assert str(excinfo.value.__cause__) == "m4" + + +@pytest.mark.parametrize("exc", [ValueError, SystemExit]) +def test_wrapper_exception(exc: "Type[BaseException]") -> None: + out = [] + + @hookimpl(wrapper=True) + def m1(): + out.append("m1 init") + try: + result = yield + except BaseException as e: + assert isinstance(e, exc) + raise + finally: + out.append("m1 finish") + return result + + @hookimpl + def m2(): + out.append("m2 init") + raise exc + + with pytest.raises(exc): + MC([m2, m1], {}) + assert out == ["m1 init", "m2 init", "m1 finish"] + + +def test_wrapper_exception_chaining() -> None: + @hookimpl + def m1(): + raise Exception("m1") + + @hookimpl(wrapper=True) + def m2(): + try: + yield + except Exception: + raise Exception("m2") + + @hookimpl(wrapper=True) + def m3(): + yield + return 10 + + @hookimpl(wrapper=True) + def m4(): + try: + yield + except Exception as e: + raise Exception("m4") from e + + with pytest.raises(Exception) as excinfo: + MC([m1, m2, m3, m4], {}) + assert str(excinfo.value) == "m4" + assert excinfo.value.__cause__ is not None + assert str(excinfo.value.__cause__) == "m2" + assert excinfo.value.__cause__.__context__ is not None + assert str(excinfo.value.__cause__.__context__) == "m1" + + +def test_unwind_inner_wrapper_teardown_exc() -> None: + out = [] + + @hookimpl(wrapper=True) + def m1(): + out.append("m1 init") + try: + yield + out.append("m1 unreachable") + except BaseException: + out.append("m1 teardown") + raise + finally: + out.append("m1 cleanup") + + @hookimpl(wrapper=True) + def m2(): + out.append("m2 init") + yield + out.append("m2 raise") + raise ValueError() + + with pytest.raises(ValueError): + try: + MC([m2, m1], {}) + finally: + out.append("finally") + + assert out == [ + "m1 init", + "m2 init", + "m2 raise", + "m1 teardown", + "m1 cleanup", + "finally", + ] + + +def test_suppress_inner_wrapper_teardown_exc() -> None: + out = [] + + @hookimpl(wrapper=True) + def m1(): + out.append("m1 init") + result = yield + out.append("m1 finish") + return result + + @hookimpl(wrapper=True) + def m2(): + out.append("m2 init") + try: + yield + out.append("m2 unreachable") + except ValueError: + out.append("m2 suppress") + return 22 + + @hookimpl(wrapper=True) + def m3(): + out.append("m3 init") + yield + out.append("m3 raise") + raise ValueError() + + assert MC([m3, m2, m1], {}) == 22 + assert out == [ + "m1 init", + "m2 init", + "m3 init", + "m3 raise", + "m2 suppress", + "m1 finish", + ] diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_pluginmanager.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_pluginmanager.py index 304a007a589..c4ce08f3bc3 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_pluginmanager.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_pluginmanager.py @@ -1,22 +1,25 @@ """ ``PluginManager`` unit and public API testing. """ + +import importlib.metadata +from typing import Any +from typing import List + import pytest -from pluggy import ( - PluginValidationError, - HookCallError, - HookimplMarker, - HookspecMarker, -) -from pluggy._manager import importlib_metadata +from pluggy import HookCallError +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager +from pluggy import PluginValidationError hookspec = HookspecMarker("example") hookimpl = HookimplMarker("example") -def test_plugin_double_register(pm): +def test_plugin_double_register(pm: PluginManager) -> None: """Registering the same plugin more then once isn't allowed""" pm.register(42, name="abc") with pytest.raises(ValueError): @@ -25,7 +28,7 @@ def test_plugin_double_register(pm): pm.register(42, name="def") -def test_pm(pm): +def test_pm(pm: PluginManager) -> None: """Basic registration with objects""" class A: @@ -43,12 +46,12 @@ def test_pm(pm): assert pm.unregister(a1) == a1 assert not pm.is_registered(a1) - out = pm.list_name_plugin() - assert len(out) == 1 - assert out == [("hello", a2)] + out2 = pm.list_name_plugin() + assert len(out2) == 1 + assert out2 == [("hello", a2)] -def test_has_plugin(pm): +def test_has_plugin(pm: PluginManager) -> None: class A: pass @@ -58,7 +61,7 @@ def test_has_plugin(pm): assert pm.has_plugin("hello") -def test_register_dynamic_attr(he_pm): +def test_register_dynamic_attr(he_pm: PluginManager) -> None: class A: def __getattr__(self, name): if name[0] != "_": @@ -70,7 +73,7 @@ def test_register_dynamic_attr(he_pm): assert not he_pm.get_hookcallers(a) -def test_pm_name(pm): +def test_pm_name(pm: PluginManager) -> None: class A: pass @@ -78,37 +81,49 @@ def test_pm_name(pm): name = pm.register(a1, name="hello") assert name == "hello" pm.unregister(a1) - assert pm.get_plugin(a1) is None + assert pm.get_plugin("hello") is None assert not pm.is_registered(a1) assert not pm.get_plugins() name2 = pm.register(a1, name="hello") assert name2 == name pm.unregister(name="hello") - assert pm.get_plugin(a1) is None + assert pm.get_plugin("hello") is None assert not pm.is_registered(a1) assert not pm.get_plugins() -def test_set_blocked(pm): +def test_set_blocked(pm: PluginManager) -> None: class A: pass a1 = A() name = pm.register(a1) + assert name is not None assert pm.is_registered(a1) assert not pm.is_blocked(name) + assert pm.get_plugins() == {a1} + pm.set_blocked(name) assert pm.is_blocked(name) assert not pm.is_registered(a1) + assert pm.get_plugins() == set() pm.set_blocked("somename") assert pm.is_blocked("somename") assert not pm.register(A(), "somename") pm.unregister(name="somename") assert pm.is_blocked("somename") + assert pm.get_plugins() == set() + + # Unblock. + assert not pm.unblock("someothername") + assert pm.unblock("somename") + assert not pm.is_blocked("somename") + assert not pm.unblock("somename") + assert pm.register(A(), "somename") -def test_register_mismatch_method(he_pm): +def test_register_mismatch_method(he_pm: PluginManager) -> None: class hello: @hookimpl def he_method_notexists(self): @@ -122,7 +137,7 @@ def test_register_mismatch_method(he_pm): assert excinfo.value.plugin is plugin -def test_register_mismatch_arg(he_pm): +def test_register_mismatch_arg(he_pm: PluginManager) -> None: class hello: @hookimpl def he_method1(self, qlwkje): @@ -135,7 +150,7 @@ def test_register_mismatch_arg(he_pm): assert excinfo.value.plugin is plugin -def test_register_hookwrapper_not_a_generator_function(he_pm): +def test_register_hookwrapper_not_a_generator_function(he_pm: PluginManager) -> None: class hello: @hookimpl(hookwrapper=True) def he_method1(self): @@ -148,31 +163,51 @@ def test_register_hookwrapper_not_a_generator_function(he_pm): assert excinfo.value.plugin is plugin -def test_register(pm): +def test_register_both_wrapper_and_hookwrapper(he_pm: PluginManager) -> None: + class hello: + @hookimpl(wrapper=True, hookwrapper=True) + def he_method1(self): + yield # pragma: no cover + + plugin = hello() + + with pytest.raises( + PluginValidationError, match="wrapper.*hookwrapper.*mutually exclusive" + ) as excinfo: + he_pm.register(plugin) + assert excinfo.value.plugin is plugin + + +def test_register(pm: PluginManager) -> None: class MyPlugin: - pass + @hookimpl + def he_method1(self): ... my = MyPlugin() pm.register(my) - assert my in pm.get_plugins() + assert pm.get_plugins() == {my} my2 = MyPlugin() pm.register(my2) - assert {my, my2}.issubset(pm.get_plugins()) + assert pm.get_plugins() == {my, my2} assert pm.is_registered(my) assert pm.is_registered(my2) pm.unregister(my) assert not pm.is_registered(my) - assert my not in pm.get_plugins() + assert pm.get_plugins() == {my2} + with pytest.raises(AssertionError, match=r"not registered"): + pm.unregister(my) -def test_register_unknown_hooks(pm): + +def test_register_unknown_hooks(pm: PluginManager) -> None: class Plugin1: @hookimpl def he_method1(self, arg): return arg + 1 pname = pm.register(Plugin1()) + assert pname is not None class Hooks: @hookspec @@ -182,10 +217,12 @@ def test_register_unknown_hooks(pm): pm.add_hookspecs(Hooks) # assert not pm._unverified_hooks assert pm.hook.he_method1(arg=1) == [2] - assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1 + hookcallers = pm.get_hookcallers(pm.get_plugin(pname)) + assert hookcallers is not None + assert len(hookcallers) == 1 -def test_register_historic(pm): +def test_register_historic(pm: PluginManager) -> None: class Hooks: @hookspec(historic=True) def he_method1(self, arg): @@ -215,21 +252,52 @@ def test_register_historic(pm): assert out == [1, 10, 120, 12] +def test_historic_with_subset_hook_caller(pm: PluginManager) -> None: + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): ... + + pm.add_hookspecs(Hooks) + + out = [] + + class Plugin: + @hookimpl + def he_method1(self, arg): + out.append(arg) + + plugin = Plugin() + pm.register(plugin) + + class Plugin2: + @hookimpl + def he_method1(self, arg): + out.append(arg * 10) + + shc = pm.subset_hook_caller("he_method1", remove_plugins=[plugin]) + shc.call_historic(kwargs=dict(arg=1)) + + pm.register(Plugin2()) + assert out == [10] + + pm.register(Plugin()) + assert out == [10, 1] + + @pytest.mark.parametrize("result_callback", [True, False]) -def test_with_result_memorized(pm, result_callback): - """Verify that ``_HookCaller._maybe_apply_history()` +def test_with_result_memorized(pm: PluginManager, result_callback: bool) -> None: + """Verify that ``HookCaller._maybe_apply_history()` correctly applies the ``result_callback`` function, when provided, to the result from calling each newly registered hook. """ out = [] - if result_callback: + if not result_callback: + callback = None + else: - def callback(res): + def callback(res) -> None: out.append(res) - else: - callback = None - class Hooks: @hookspec(historic=True) def he_method1(self, arg): @@ -259,7 +327,7 @@ def test_with_result_memorized(pm, result_callback): assert out == [] -def test_with_callbacks_immediately_executed(pm): +def test_with_callbacks_immediately_executed(pm: PluginManager) -> None: class Hooks: @hookspec(historic=True) def he_method1(self, arg): @@ -293,7 +361,7 @@ def test_with_callbacks_immediately_executed(pm): assert out == [20, 10, 30] -def test_register_historic_incompat_hookwrapper(pm): +def test_register_historic_incompat_hookwrapper(pm: PluginManager) -> None: class Hooks: @hookspec(historic=True) def he_method1(self, arg): @@ -312,7 +380,24 @@ def test_register_historic_incompat_hookwrapper(pm): pm.register(Plugin()) -def test_call_extra(pm): +def test_register_historic_incompat_wrapper(pm: PluginManager) -> None: + class Hooks: + @hookspec(historic=True) + def he_method1(self, arg): + pass + + pm.add_hookspecs(Hooks) + + class Plugin: + @hookimpl(wrapper=True) + def he_method1(self, arg): + yield + + with pytest.raises(PluginValidationError): + pm.register(Plugin()) + + +def test_call_extra(pm: PluginManager) -> None: class Hooks: @hookspec def he_method1(self, arg): @@ -327,7 +412,7 @@ def test_call_extra(pm): assert out == [10] -def test_call_with_too_few_args(pm): +def test_call_with_too_few_args(pm: PluginManager) -> None: class Hooks: @hookspec def he_method1(self, arg): @@ -346,7 +431,7 @@ def test_call_with_too_few_args(pm): pm.hook.he_method1() -def test_subset_hook_caller(pm): +def test_subset_hook_caller(pm: PluginManager) -> None: class Hooks: @hookspec def he_method1(self, arg): @@ -395,8 +480,10 @@ def test_subset_hook_caller(pm): pm.hook.he_method1(arg=1) assert out == [10] + assert repr(hc) == "<_SubsetHookCaller 'he_method1'>" -def test_get_hookimpls(pm): + +def test_get_hookimpls(pm: PluginManager) -> None: class Hooks: @hookspec def he_method1(self, arg): @@ -428,12 +515,61 @@ def test_get_hookimpls(pm): assert hook_plugins == [plugin1, plugin2] -def test_add_hookspecs_nohooks(pm): +def test_get_hookcallers(pm: PluginManager) -> None: + class Hooks: + @hookspec + def he_method1(self): ... + + @hookspec + def he_method2(self): ... + + pm.add_hookspecs(Hooks) + + class Plugin1: + @hookimpl + def he_method1(self): ... + + @hookimpl + def he_method2(self): ... + + class Plugin2: + @hookimpl + def he_method1(self): ... + + class Plugin3: + @hookimpl + def he_method2(self): ... + + plugin1 = Plugin1() + pm.register(plugin1) + plugin2 = Plugin2() + pm.register(plugin2) + plugin3 = Plugin3() + pm.register(plugin3) + + hookcallers1 = pm.get_hookcallers(plugin1) + assert hookcallers1 is not None + assert len(hookcallers1) == 2 + hookcallers2 = pm.get_hookcallers(plugin2) + assert hookcallers2 is not None + assert len(hookcallers2) == 1 + hookcallers3 = pm.get_hookcallers(plugin3) + assert hookcallers3 is not None + assert len(hookcallers3) == 1 + assert hookcallers1 == hookcallers2 + hookcallers3 + + assert pm.get_hookcallers(object()) is None + + +def test_add_hookspecs_nohooks(pm: PluginManager) -> None: + class NoHooks: + pass + with pytest.raises(ValueError): - pm.add_hookspecs(10) + pm.add_hookspecs(NoHooks) -def test_load_setuptools_instantiation(monkeypatch, pm): +def test_load_setuptools_instantiation(monkeypatch, pm: PluginManager) -> None: class EntryPoint: name = "myname" group = "hello" @@ -453,23 +589,24 @@ def test_load_setuptools_instantiation(monkeypatch, pm): def my_distributions(): return (dist,) - monkeypatch.setattr(importlib_metadata, "distributions", my_distributions) + monkeypatch.setattr(importlib.metadata, "distributions", my_distributions) num = pm.load_setuptools_entrypoints("hello") assert num == 1 plugin = pm.get_plugin("myname") + assert plugin is not None assert plugin.x == 42 ret = pm.list_plugin_distinfo() # poor man's `assert ret == [(plugin, mock.ANY)]` assert len(ret) == 1 assert len(ret[0]) == 2 assert ret[0][0] == plugin - assert ret[0][1]._dist == dist + assert ret[0][1]._dist == dist # type: ignore[comparison-overlap] num = pm.load_setuptools_entrypoints("hello") assert num == 0 # no plugin loaded by this call -def test_add_tracefuncs(he_pm): - out = [] +def test_add_tracefuncs(he_pm: PluginManager) -> None: + out: List[Any] = [] class api1: @hookimpl @@ -507,7 +644,7 @@ def test_add_tracefuncs(he_pm): assert len(out) == 4 + 2 -def test_hook_tracing(he_pm): +def test_hook_tracing(he_pm: PluginManager) -> None: saveindent = [] class api1: @@ -522,7 +659,7 @@ def test_hook_tracing(he_pm): raise ValueError() he_pm.register(api1()) - out = [] + out: List[Any] = [] he_pm.trace.root.setwriter(out.append) undo = he_pm.enable_tracing() try: @@ -542,3 +679,79 @@ def test_hook_tracing(he_pm): assert saveindent[0] > indent finally: undo() + + +@pytest.mark.parametrize("historic", [False, True]) +def test_register_while_calling( + pm: PluginManager, + historic: bool, +) -> None: + """Test that registering an impl of a hook while it is being called does + not affect the call itself, only later calls. + + For historic hooks however, the hook is called immediately on registration. + + Regression test for #438. + """ + hookspec = HookspecMarker("example") + + class Hooks: + @hookspec(historic=historic) + def configure(self) -> int: + raise NotImplementedError() + + class Plugin1: + @hookimpl + def configure(self) -> int: + return 1 + + class Plugin2: + def __init__(self) -> None: + self.already_registered = False + + @hookimpl + def configure(self) -> int: + if not self.already_registered: + pm.register(Plugin4()) + pm.register(Plugin5()) + pm.register(Plugin6()) + self.already_registered = True + return 2 + + class Plugin3: + @hookimpl + def configure(self) -> int: + return 3 + + class Plugin4: + @hookimpl(tryfirst=True) + def configure(self) -> int: + return 4 + + class Plugin5: + @hookimpl() + def configure(self) -> int: + return 5 + + class Plugin6: + @hookimpl(trylast=True) + def configure(self) -> int: + return 6 + + pm.add_hookspecs(Hooks) + pm.register(Plugin1()) + pm.register(Plugin2()) + pm.register(Plugin3()) + + if not historic: + result = pm.hook.configure() + assert result == [3, 2, 1] + result = pm.hook.configure() + assert result == [4, 5, 3, 2, 1, 6] + else: + result = [] + pm.hook.configure.call_historic(result.append) + assert result == [4, 5, 6, 3, 2, 1] + result = [] + pm.hook.configure.call_historic(result.append) + assert result == [4, 5, 3, 2, 1, 6] diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_tracer.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_tracer.py index 992ec679149..5e538369446 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/testing/test_tracer.py +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_tracer.py @@ -1,17 +1,19 @@ -from pluggy._tracing import TagTracer +from typing import List import pytest +from pluggy._tracing import TagTracer + @pytest.fixture -def rootlogger(): +def rootlogger() -> TagTracer: return TagTracer() -def test_simple(rootlogger): +def test_simple(rootlogger: TagTracer) -> None: log = rootlogger.get("pytest") log("hello") - out = [] + out: List[str] = [] rootlogger.setwriter(out.append) log("world") assert len(out) == 1 @@ -21,7 +23,7 @@ def test_simple(rootlogger): assert out[1] == "hello [pytest:collection]\n" -def test_indent(rootlogger): +def test_indent(rootlogger: TagTracer) -> None: log = rootlogger.get("1") out = [] log.root.setwriter(lambda arg: out.append(arg)) @@ -49,8 +51,7 @@ def test_indent(rootlogger): ] -def test_readable_output_dictargs(rootlogger): - +def test_readable_output_dictargs(rootlogger: TagTracer) -> None: out = rootlogger._format_message(["test"], [1]) assert out == "1 [test]\n" @@ -58,7 +59,7 @@ def test_readable_output_dictargs(rootlogger): assert out2 == "test [test]\n a: 1\n" -def test_setprocessor(rootlogger): +def test_setprocessor(rootlogger: TagTracer) -> None: log = rootlogger.get("1") log2 = log.get("2") assert log2.tags == tuple("12") diff --git a/tests/wpt/tests/tools/third_party/pluggy/testing/test_warnings.py b/tests/wpt/tests/tools/third_party/pluggy/testing/test_warnings.py new file mode 100644 index 00000000000..4f5454be24f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pluggy/testing/test_warnings.py @@ -0,0 +1,49 @@ +from pathlib import Path + +import pytest + +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluggyTeardownRaisedWarning +from pluggy import PluginManager + + +hookspec = HookspecMarker("example") +hookimpl = HookimplMarker("example") + + +def test_teardown_raised_warning(pm: PluginManager) -> None: + class Api: + @hookspec + def my_hook(self): + raise NotImplementedError() + + pm.add_hookspecs(Api) + + class Plugin1: + @hookimpl + def my_hook(self): + pass + + class Plugin2: + @hookimpl(hookwrapper=True) + def my_hook(self): + yield + 1 / 0 + + class Plugin3: + @hookimpl(hookwrapper=True) + def my_hook(self): + yield + + pm.register(Plugin1(), "plugin1") + pm.register(Plugin2(), "plugin2") + pm.register(Plugin3(), "plugin3") + with pytest.warns( + PluggyTeardownRaisedWarning, + match=r"\bplugin2\b.*\bmy_hook\b.*\n.*ZeroDivisionError", + ) as wc: + with pytest.raises(ZeroDivisionError): + pm.hook.my_hook() + assert len(wc.list) == 1 + assert Path(wc.list[0].filename).name == "test_warnings.py" diff --git a/tests/wpt/tests/tools/third_party/pluggy/tox.ini b/tests/wpt/tests/tools/third_party/pluggy/tox.ini index 97b3eb77920..de464a078f1 100644 --- a/tests/wpt/tests/tools/third_party/pluggy/tox.ini +++ b/tests/wpt/tests/tools/third_party/pluggy/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=linting,docs,py{36,37,38,39,py3},py{36,37}-pytest{main} +envlist=docs,py{38,39,310,311,py3},py{38}-pytest{main} [testenv] commands= @@ -20,18 +20,15 @@ deps= pytest pytest-benchmark -[testenv:linting] -skip_install = true -basepython = python3 -deps = pre-commit -commands = pre-commit run --all-files --show-diff-on-failure - [testenv:docs] deps = - sphinx - pygments + -r{toxinidir}/docs/requirements.txt commands = - sphinx-build -W -b html {toxinidir}/docs {toxinidir}/build/html-docs + python scripts/towncrier-draft-to-file.py + # the '-t changelog_towncrier_draft' tags makes sphinx include the draft + # changelog in the docs; this does not happen on ReadTheDocs because it uses + # the standard sphinx command so the 'changelog_towncrier_draft' is never set there + sphinx-build -W -b html {toxinidir}/docs {toxinidir}/build/html-docs -t changelog_towncrier_draft {posargs:} [pytest] minversion=2.0 @@ -45,7 +42,7 @@ filterwarnings = max-line-length=99 [testenv:release] -decription = do a release, required posarg of the version number +description = do a release, required posarg of the version number basepython = python3 skipsdist = True usedevelop = True diff --git a/tests/wpt/tests/tools/third_party/py/.flake8 b/tests/wpt/tests/tools/third_party/py/.flake8 deleted file mode 100644 index f9c71a7fbc4..00000000000 --- a/tests/wpt/tests/tools/third_party/py/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -per-file-ignores = - **/*.pyi:E252,E301,E302,E305,E501,E701,E704,F401,F811,F821 diff --git a/tests/wpt/tests/tools/third_party/py/.gitattributes b/tests/wpt/tests/tools/third_party/py/.gitattributes deleted file mode 100644 index 1246879c4da..00000000000 --- a/tests/wpt/tests/tools/third_party/py/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.dump eol=lf diff --git a/tests/wpt/tests/tools/third_party/py/.github/workflows/main.yml b/tests/wpt/tests/tools/third_party/py/.github/workflows/main.yml deleted file mode 100644 index 564aa0c5315..00000000000 --- a/tests/wpt/tests/tools/third_party/py/.github/workflows/main.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: build - -on: [push, pull_request] - -jobs: - build: - - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - python: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy3"] - os: [ubuntu-latest, windows-latest] - include: - - python: "2.7" - tox_env: "py27-pytest30" - - python: "3.5" - tox_env: "py35-pytest30" - - python: "3.6" - tox_env: "py36-pytest30" - - python: "3.7" - tox_env: "py37-pytest30" - - python: "3.8" - tox_env: "py38-pytest30" - - python: "3.9" - tox_env: "py39-pytest30" - - python: "pypy3" - tox_env: "pypy3-pytest30" - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - name: Test - run: | - pipx run tox -e ${{ matrix.tox_env }} - - deploy: - - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - - runs-on: ubuntu-latest - - needs: build - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.7" - - name: Install wheel - run: | - python -m pip install --upgrade pip - pip install wheel - - name: Build package - run: | - python setup.py sdist bdist_wheel - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_token }} diff --git a/tests/wpt/tests/tools/third_party/py/.gitignore b/tests/wpt/tests/tools/third_party/py/.gitignore deleted file mode 100644 index fa936f1596d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ - -.cache/ -.tox/ -__pycache__/ -.mypy_cache/ - -*.pyc -*.pyo - -*.egg-info -.eggs/ - -dist/* -/py/_version.py -.pytest_cache/ diff --git a/tests/wpt/tests/tools/third_party/py/AUTHORS b/tests/wpt/tests/tools/third_party/py/AUTHORS deleted file mode 100644 index 9c5dda9ceb0..00000000000 --- a/tests/wpt/tests/tools/third_party/py/AUTHORS +++ /dev/null @@ -1,25 +0,0 @@ -Holger Krekel, holger at merlinux eu -Benjamin Peterson, benjamin at python org -Ronny Pfannschmidt, Ronny.Pfannschmidt at gmx de -Guido Wesdorp, johnny at johnnydebris net -Samuele Pedroni, pedronis at openend se -Carl Friedrich Bolz, cfbolz at gmx de -Armin Rigo, arigo at tunes org -Maciek Fijalkowski, fijal at genesilico pl -Brian Dorsey, briandorsey at gmail com -Floris Bruynooghe, flub at devork be -merlinux GmbH, Germany, office at merlinux eu - -Contributors include:: - -Ross Lawley -Ralf Schmitt -Chris Lamb -Harald Armin Massa -Martijn Faassen -Ian Bicking -Jan Balster -Grig Gheorghiu -Bob Ippolito -Christian Tismer -Wim Glenn diff --git a/tests/wpt/tests/tools/third_party/py/CHANGELOG.rst b/tests/wpt/tests/tools/third_party/py/CHANGELOG.rst deleted file mode 100644 index 47c6fdb7a17..00000000000 --- a/tests/wpt/tests/tools/third_party/py/CHANGELOG.rst +++ /dev/null @@ -1,1236 +0,0 @@ -1.11.0 (2021-11-04) -=================== - -- Support Python 3.11 -- Support ``NO_COLOR`` environment variable -- Update vendored apipkg: 1.5 => 2.0 - -1.10.0 (2020-12-12) -=================== - -- Fix a regular expression DoS vulnerability in the py.path.svnwc SVN blame functionality (CVE-2020-29651) -- Update vendored apipkg: 1.4 => 1.5 -- Update vendored iniconfig: 1.0.0 => 1.1.1 - -1.9.0 (2020-06-24) -================== - -- Add type annotation stubs for the following modules: - - * ``py.error`` - * ``py.iniconfig`` - * ``py.path`` (not including SVN paths) - * ``py.io`` - * ``py.xml`` - - There are no plans to type other modules at this time. - - The type annotations are provided in external .pyi files, not inline in the - code, and may therefore contain small errors or omissions. If you use ``py`` - in conjunction with a type checker, and encounter any type errors you believe - should be accepted, please report it in an issue. - -1.8.2 (2020-06-15) -================== - -- On Windows, ``py.path.local``s which differ only in case now have the same - Python hash value. Previously, such paths were considered equal but had - different hashes, which is not allowed and breaks the assumptions made by - dicts, sets and other users of hashes. - -1.8.1 (2019-12-27) -================== - -- Handle ``FileNotFoundError`` when trying to import pathlib in ``path.common`` - on Python 3.4 (#207). - -- ``py.path.local.samefile`` now works correctly in Python 3 on Windows when dealing with symlinks. - -1.8.0 (2019-02-21) -================== - -- add ``"importlib"`` pyimport mode for python3.5+, allowing unimportable test suites - to contain identically named modules. - -- fix ``LocalPath.as_cwd()`` not calling ``os.chdir()`` with ``None``, when - being invoked from a non-existing directory. - - -1.7.0 (2018-10-11) -================== - -- fix #174: use ``shutil.get_terminal_size()`` in Python 3.3+ to determine the size of the - terminal, which produces more accurate results than the previous method. - -- fix pytest-dev/pytest#2042: introduce new ``PY_IGNORE_IMPORTMISMATCH`` environment variable - that suppresses ``ImportMismatchError`` exceptions when set to ``1``. - - -1.6.0 (2018-08-27) -================== - -- add ``TerminalWriter.width_of_current_line`` (i18n version of - ``TerminalWriter.chars_on_current_line``), a read-only property - that tracks how wide the current line is, attempting to take - into account international characters in the calculation. - -1.5.4 (2018-06-27) -================== - -- fix pytest-dev/pytest#3451: don't make assumptions about fs case sensitivity - in ``make_numbered_dir``. - -1.5.3 -===== - -- fix #179: ensure we can support 'from py.error import ...' - -1.5.2 -===== - -- fix #169, #170: error importing py.log on Windows: no module named ``syslog``. - -1.5.1 -===== - -- fix #167 - prevent pip from installing py in unsupported Python versions. - -1.5.0 -===== - -NOTE: **this release has been removed from PyPI** due to missing package -metadata which caused a number of problems to py26 and py33 users. -This issue was fixed in the 1.5.1 release. - -- python 2.6 and 3.3 are no longer supported -- deprecate py.std and remove all internal uses -- fix #73 turn py.error into an actual module -- path join to / no longer produces leading double slashes -- fix #82 - remove unsupportable aliases -- fix python37 compatibility of path.sysfind on windows by correctly replacing vars -- turn iniconfig and apipkg into vendored packages and ease de-vendoring for distributions -- fix #68 remove invalid py.test.ensuretemp references -- fix #25 - deprecate path.listdir(sort=callable) -- add ``TerminalWriter.chars_on_current_line`` read-only property that tracks how many characters - have been written to the current line. - -1.4.34 -==================================================================== - -- fix issue119 / pytest issue708 where tmpdir may fail to make numbered directories - when the filesystem is case-insensitive. - -1.4.33 -==================================================================== - -- avoid imports in calls to py.path.local().fnmatch(). Thanks Andreas Pelme for - the PR. - -- fix issue106: Naive unicode encoding when calling fspath() in python2. Thanks Tiago Nobrega for the PR. - -- fix issue110: unittest.TestCase.assertWarns fails with py imported. - -1.4.32 -==================================================================== - -- fix issue70: added ability to copy all stat info in py.path.local.copy. - -- make TerminalWriter.fullwidth a property. This results in the correct - value when the terminal gets resized. - -- update supported html tags to include recent additions. - Thanks Denis Afonso for the PR. - -- Remove internal code in ``Source.compile`` meant to support earlier Python 3 versions that produced the side effect - of leaving ``None`` in ``sys.modules`` when called (see pytest-dev/pytest#2103). - Thanks Bruno Oliveira for the PR. - -1.4.31 -================================================== - -- fix local().copy(dest, mode=True) to also work - with unicode. - -- pass better error message with svn EEXIST paths - -1.4.30 -================================================== - -- fix issue68 an assert with a multiline list comprehension - was not reported correctly. Thanks Henrik Heibuerger. - - -1.4.29 -================================================== - -- fix issue55: revert a change to the statement finding algorithm - which is used by pytest for generating tracebacks. - Thanks Daniel Hahler for initial analysis. - -- fix pytest issue254 for when traceback rendering can't - find valid source code. Thanks Ionel Cristian Maries. - - -1.4.28 -================================================== - -- fix issue64 -- dirpath regression when "abs=True" is passed. - Thanks Gilles Dartiguelongue. - -1.4.27 -================================================== - -- fix issue59: point to new repo site - -- allow a new ensuresyspath="append" mode for py.path.local.pyimport() - so that a neccessary import path is appended instead of prepended to - sys.path - -- strike undocumented, untested argument to py.path.local.pypkgpath - -- speed up py.path.local.dirpath by a factor of 10 - -1.4.26 -================================================== - -- avoid calling normpath twice in py.path.local - -- py.builtin._reraise properly reraises under Python3 now. - -- fix issue53 - remove module index, thanks jenisys. - -- allow posix path separators when "fnmatch" is called. - Thanks Christian Long for the complete PR. - -1.4.25 -================================================== - -- fix issue52: vaguely fix py25 compat of py.path.local (it's not - officially supported), also fix docs - -- fix pytest issue 589: when checking if we have a recursion error - check for the specific "maximum recursion depth" text of the exception. - -1.4.24 -================================================== - -- Fix retrieving source when an else: line has an other statement on - the same line. - -- add localpath read_text/write_text/read_bytes/write_bytes methods - as shortcuts and clearer bytes/text interfaces for read/write. - Adapted from a PR from Paul Moore. - - -1.4.23 -================================================== - -- use newer apipkg version which makes attribute access on - alias modules resolve to None rather than an ImportError. - This helps with code that uses inspect.getframeinfo() - on py34 which causes a complete walk on sys.modules - thus triggering the alias module to resolve and blowing - up with ImportError. The negative side is that something - like "py.test.X" will now result in None instead of "importerror: pytest" - if pytest is not installed. But you shouldn't import "py.test" - anyway anymore. - -- adapt one svn test to only check for any exception instead - of specific ones because different svn versions cause different - errors and we don't care. - - -1.4.22 -================================================== - -- refactor class-level registry on ForkedFunc child start/finish - event to become instance based (i.e. passed into the constructor) - -1.4.21 -================================================== - -- ForkedFunc now has class-level register_on_start/on_exit() - methods to allow adding information in the boxed process. - Thanks Marc Schlaich. - -- ForkedFunc in the child opens in "auto-flush" mode for - stdout/stderr so that when a subprocess dies you can see - its output even if it didn't flush itself. - -- refactor traceback generation in light of pytest issue 364 - (shortening tracebacks). you can now set a new traceback style - on a per-entry basis such that a caller can force entries to be - isplayed as short or long entries. - -- win32: py.path.local.sysfind(name) will preferrably return files with - extensions so that if "X" and "X.bat" or "X.exe" is on the PATH, - one of the latter two will be returned. - -1.4.20 -================================================== - -- ignore unicode decode errors in xmlescape. Thanks Anatoly Bubenkoff. - -- on python2 modify traceback.format_exception_only to match python3 - behaviour, namely trying to print unicode for Exception instances - -- use a safer way for serializing exception reports (helps to fix - pytest issue413) - -Changes between 1.4.18 and 1.4.19 -================================================== - -- merge in apipkg fixes - -- some micro-optimizations in py/_code/code.py for speeding - up pytest runs. Thanks Alex Gaynor for initiative. - -- check PY_COLORS=1 or PY_COLORS=0 to force coloring/not-coloring - for py.io.TerminalWriter() independently from capabilities - of the output file. Thanks Marc Abramowitz for the PR. - -- some fixes to unicode handling in assertion handling. - Thanks for the PR to Floris Bruynooghe. (This helps - to fix pytest issue 319). - -- depend on setuptools presence, remove distribute_setup - -Changes between 1.4.17 and 1.4.18 -================================================== - -- introduce path.ensure_dir() as a synonym for ensure(..., dir=1) - -- some unicode/python3 related fixes wrt to path manipulations - (if you start passing unicode particular in py2 you might - still get problems, though) - -Changes between 1.4.16 and 1.4.17 -================================================== - -- make py.io.TerminalWriter() prefer colorama if it is available - and avoid empty lines when separator-lines are printed by - being defensive and reducing the working terminalwidth by 1 - -- introduce optional "expanduser" argument to py.path.local - to that local("~", expanduser=True) gives the home - directory of "user". - -Changes between 1.4.15 and 1.4.16 -================================================== - -- fix issue35 - define __gt__ ordering between a local path - and strings - -- fix issue36 - make chdir() work even if os.getcwd() fails. - -- add path.exists/isdir/isfile/islink shortcuts - -- introduce local path.as_cwd() context manager. - -- introduce p.write(ensure=1) and p.open(ensure=1) - where ensure triggers creation of neccessary parent - dirs. - - -Changes between 1.4.14 and 1.4.15 -================================================== - -- majorly speed up some common calling patterns with - LocalPath.listdir()/join/check/stat functions considerably. - -- fix an edge case with fnmatch where a glob style pattern appeared - in an absolute path. - -Changes between 1.4.13 and 1.4.14 -================================================== - -- fix dupfile to work with files that don't - carry a mode. Thanks Jason R. Coombs. - -Changes between 1.4.12 and 1.4.13 -================================================== - -- fix getting statementrange/compiling a file ending - in a comment line without newline (on python2.5) -- for local paths you can pass "mode=True" to a copy() - in order to copy permission bits (underlying mechanism - is using shutil.copymode) -- add paths arguments to py.path.local.sysfind to restrict - search to the diretories in the path. -- add isdir/isfile/islink to path.stat() objects allowing to perform - multiple checks without calling out multiple times -- drop py.path.local.__new__ in favour of a simpler __init__ -- iniconfig: allow "name:value" settings in config files, no space after - "name" required -- fix issue 27 - NameError in unlikely untested case of saferepr - - -Changes between 1.4.11 and 1.4.12 -================================================== - -- fix python2.4 support - for pre-AST interpreters re-introduce - old way to find statements in exceptions (closes pytest issue 209) -- add tox.ini to distribution -- fix issue23 - print *,** args information in tracebacks, - thanks Manuel Jacob - - -Changes between 1.4.10 and 1.4.11 -================================================== - -- use _ast to determine statement ranges when printing tracebacks - - avoiding multi-second delays on some large test modules -- fix an internal test to not use class-denoted pytest_funcarg__ -- fix a doc link to bug tracker -- try to make terminal.write() printing more robust against - unicodeencode/decode problems, amend according test -- introduce py.builtin.text and py.builtin.bytes - to point to respective str/unicode (py2) and bytes/str (py3) types -- fix error handling on win32/py33 for ENODIR - -Changes between 1.4.9 and 1.4.10 -================================================== - -- terminalwriter: default to encode to UTF8 if no encoding is defined - on the output stream -- issue22: improve heuristic for finding the statementrange in exceptions - -Changes between 1.4.8 and 1.4.9 -================================================== - -- fix bug of path.visit() which would not recognize glob-style patterns - for the "rec" recursion argument -- changed iniconfig parsing to better conform, now the chars ";" - and "#" only mark a comment at the stripped start of a line -- include recent apipkg-1.2 -- change internal terminalwriter.line/reline logic to more nicely - support file spinners - -Changes between 1.4.7 and 1.4.8 -================================================== - -- fix issue 13 - correct handling of the tag name object in xmlgen -- fix issue 14 - support raw attribute values in xmlgen -- fix windows terminalwriter printing/re-line problem -- update distribute_setup.py to 0.6.27 - -Changes between 1.4.6 and 1.4.7 -================================================== - -- fix issue11 - own test failure with python3.3 / Thanks Benjamin Peterson -- help fix pytest issue 102 - -Changes between 1.4.5 and 1.4.6 -================================================== - -- help to fix pytest issue99: unify output of - ExceptionInfo.getrepr(style="native") with ...(style="long") -- fix issue7: source.getstatementrange() now raises proper error - if no valid statement can be found -- fix issue8: fix code and tests of svnurl/svnwc to work on subversion 1.7 - - note that path.status(updates=1) will not properly work svn-17's status - --xml output is broken. -- make source.getstatementrange() more resilent about non-python code frames - (as seen from jnja2) -- make trackeback recursion detection more resilent - about the eval magic of a decorator library -- iniconfig: add support for ; as comment starter -- properly handle lists in xmlgen on python3 -- normalize py.code.getfslineno(obj) to always return a (string, int) tuple - defaulting to ("", -1) respectively if no source code can be found for obj. - -Changes between 1.4.4 and 1.4.5 -================================================== - -- improve some unicode handling in terminalwriter and capturing - (used by pytest) - -Changes between 1.4.3 and 1.4.4 -================================================== - -- a few fixes and assertion related refinements for pytest-2.1 -- guard py.code.Code and getfslineno against bogus input - and make py.code.Code objects for object instance - by looking up their __call__ function. -- make exception presentation robust against invalid current cwd - -Changes between 1.4.2 and 1.4.3 -================================================== - -- fix terminal coloring issue for skipped tests (thanks Amaury) -- fix issue4 - large calls to ansi_print (thanks Amaury) - -Changes between 1.4.1 and 1.4.2 -================================================== - -- fix (pytest) issue23 - tmpdir argument now works on Python3.2 and WindowsXP - (which apparently starts to offer os.symlink now) - -- better error message for syntax errors from compiled code - -- small fix to better deal with (un-)colored terminal output on windows - -Changes between 1.4.0 and 1.4.1 -================================================== - -- fix issue1 - py.error.* classes to be pickleable - -- fix issue2 - on windows32 use PATHEXT as the list of potential - extensions to find find binaries with py.path.local.sysfind(commandname) - -- fix (pytest-) issue10 and refine assertion reinterpretation - to avoid breaking if the __nonzero__ of an object fails - -- fix (pytest-) issue17 where python3 does not like "import *" - leading to misrepresentation of import-errors in test modules - -- fix py.error.* attribute pypy access issue - -- allow path.samefile(arg) to succeed when arg is a relative filename - -- fix (pytest-) issue20 path.samefile(relpath) works as expected now - -- fix (pytest-) issue8 len(long_list) now shows the lenght of the list - -Changes between 1.3.4 and 1.4.0 -================================================== - -- py.test was moved to a separate "pytest" package. What remains is - a stub hook which will proxy ``import py.test`` to ``pytest``. -- all command line tools ("py.cleanup/lookup/countloc/..." moved - to "pycmd" package) -- removed the old and deprecated "py.magic" namespace -- use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available -- add py.iniconfig module for brain-dead easy ini-config file parsing -- introduce py.builtin.any() -- path objects have a .dirname attribute now (equivalent to - os.path.dirname(path)) -- path.visit() accepts breadthfirst (bf) and sort options -- remove deprecated py.compat namespace - -Changes between 1.3.3 and 1.3.4 -================================================== - -- fix issue111: improve install documentation for windows -- fix issue119: fix custom collectability of __init__.py as a module -- fix issue116: --doctestmodules work with __init__.py files as well -- fix issue115: unify internal exception passthrough/catching/GeneratorExit -- fix issue118: new --tb=native for presenting cpython-standard exceptions - -Changes between 1.3.2 and 1.3.3 -================================================== - -- fix issue113: assertion representation problem with triple-quoted strings - (and possibly other cases) -- make conftest loading detect that a conftest file with the same - content was already loaded, avoids surprises in nested directory structures - which can be produced e.g. by Hudson. It probably removes the need to use - --confcutdir in most cases. -- fix terminal coloring for win32 - (thanks Michael Foord for reporting) -- fix weirdness: make terminal width detection work on stdout instead of stdin - (thanks Armin Ronacher for reporting) -- remove trailing whitespace in all py/text distribution files - -Changes between 1.3.1 and 1.3.2 -================================================== - -New features -++++++++++++++++++ - -- fix issue103: introduce py.test.raises as context manager, examples:: - - with py.test.raises(ZeroDivisionError): - x = 0 - 1 / x - - with py.test.raises(RuntimeError) as excinfo: - call_something() - - # you may do extra checks on excinfo.value|type|traceback here - - (thanks Ronny Pfannschmidt) - -- Funcarg factories can now dynamically apply a marker to a - test invocation. This is for example useful if a factory - provides parameters to a test which are expected-to-fail:: - - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail(reason="flaky config")) - ... - - def test_function(arg): - ... - -- improved error reporting on collection and import errors. This makes - use of a more general mechanism, namely that for custom test item/collect - nodes ``node.repr_failure(excinfo)`` is now uniformly called so that you can - override it to return a string error representation of your choice - which is going to be reported as a (red) string. - -- introduce '--junitprefix=STR' option to prepend a prefix - to all reports in the junitxml file. - -Bug fixes / Maintenance -++++++++++++++++++++++++++ - -- make tests and the ``pytest_recwarn`` plugin in particular fully compatible - to Python2.7 (if you use the ``recwarn`` funcarg warnings will be enabled so that - you can properly check for their existence in a cross-python manner). -- refine --pdb: ignore xfailed tests, unify its TB-reporting and - don't display failures again at the end. -- fix assertion interpretation with the ** operator (thanks Benjamin Peterson) -- fix issue105 assignment on the same line as a failing assertion (thanks Benjamin Peterson) -- fix issue104 proper escaping for test names in junitxml plugin (thanks anonymous) -- fix issue57 -f|--looponfail to work with xpassing tests (thanks Ronny) -- fix issue92 collectonly reporter and --pastebin (thanks Benjamin Peterson) -- fix py.code.compile(source) to generate unique filenames -- fix assertion re-interp problems on PyPy, by defering code - compilation to the (overridable) Frame.eval class. (thanks Amaury Forgeot) -- fix py.path.local.pyimport() to work with directories -- streamline py.path.local.mkdtemp implementation and usage -- don't print empty lines when showing junitxml-filename -- add optional boolean ignore_errors parameter to py.path.local.remove -- fix terminal writing on win32/python2.4 -- py.process.cmdexec() now tries harder to return properly encoded unicode objects - on all python versions -- install plain py.test/py.which scripts also for Jython, this helps to - get canonical script paths in virtualenv situations -- make path.bestrelpath(path) return ".", note that when calling - X.bestrelpath the assumption is that X is a directory. -- make initial conftest discovery ignore "--" prefixed arguments -- fix resultlog plugin when used in an multicpu/multihost xdist situation - (thanks Jakub Gustak) -- perform distributed testing related reporting in the xdist-plugin - rather than having dist-related code in the generic py.test - distribution -- fix homedir detection on Windows -- ship distribute_setup.py version 0.6.13 - -Changes between 1.3.0 and 1.3.1 -================================================== - -New features -++++++++++++++++++ - -- issue91: introduce new py.test.xfail(reason) helper - to imperatively mark a test as expected to fail. Can - be used from within setup and test functions. This is - useful especially for parametrized tests when certain - configurations are expected-to-fail. In this case the - declarative approach with the @py.test.mark.xfail cannot - be used as it would mark all configurations as xfail. - -- issue102: introduce new --maxfail=NUM option to stop - test runs after NUM failures. This is a generalization - of the '-x' or '--exitfirst' option which is now equivalent - to '--maxfail=1'. Both '-x' and '--maxfail' will - now also print a line near the end indicating the Interruption. - -- issue89: allow py.test.mark decorators to be used on classes - (class decorators were introduced with python2.6) and - also allow to have multiple markers applied at class/module level - by specifying a list. - -- improve and refine letter reporting in the progress bar: - . pass - f failed test - s skipped tests (reminder: use for dependency/platform mismatch only) - x xfailed test (test that was expected to fail) - X xpassed test (test that was expected to fail but passed) - - You can use any combination of 'fsxX' with the '-r' extended - reporting option. The xfail/xpass results will show up as - skipped tests in the junitxml output - which also fixes - issue99. - -- make py.test.cmdline.main() return the exitstatus instead of raising - SystemExit and also allow it to be called multiple times. This of - course requires that your application and tests are properly teared - down and don't have global state. - -Fixes / Maintenance -++++++++++++++++++++++ - -- improved traceback presentation: - - improved and unified reporting for "--tb=short" option - - Errors during test module imports are much shorter, (using --tb=short style) - - raises shows shorter more relevant tracebacks - - --fulltrace now more systematically makes traces longer / inhibits cutting - -- improve support for raises and other dynamically compiled code by - manipulating python's linecache.cache instead of the previous - rather hacky way of creating custom code objects. This makes - it seemlessly work on Jython and PyPy where it previously didn't. - -- fix issue96: make capturing more resilient against Control-C - interruptions (involved somewhat substantial refactoring - to the underlying capturing functionality to avoid race - conditions). - -- fix chaining of conditional skipif/xfail decorators - so it works now - as expected to use multiple @py.test.mark.skipif(condition) decorators, - including specific reporting which of the conditions lead to skipping. - -- fix issue95: late-import zlib so that it's not required - for general py.test startup. - -- fix issue94: make reporting more robust against bogus source code - (and internally be more careful when presenting unexpected byte sequences) - - -Changes between 1.2.1 and 1.3.0 -================================================== - -- deprecate --report option in favour of a new shorter and easier to - remember -r option: it takes a string argument consisting of any - combination of 'xfsX' characters. They relate to the single chars - you see during the dotted progress printing and will print an extra line - per test at the end of the test run. This extra line indicates the exact - position or test ID that you directly paste to the py.test cmdline in order - to re-run a particular test. - -- allow external plugins to register new hooks via the new - pytest_addhooks(pluginmanager) hook. The new release of - the pytest-xdist plugin for distributed and looponfailing - testing requires this feature. - -- add a new pytest_ignore_collect(path, config) hook to allow projects and - plugins to define exclusion behaviour for their directory structure - - for example you may define in a conftest.py this method:: - - def pytest_ignore_collect(path): - return path.check(link=1) - - to prevent even a collection try of any tests in symlinked dirs. - -- new pytest_pycollect_makemodule(path, parent) hook for - allowing customization of the Module collection object for a - matching test module. - -- extend and refine xfail mechanism: - ``@py.test.mark.xfail(run=False)`` do not run the decorated test - ``@py.test.mark.xfail(reason="...")`` prints the reason string in xfail summaries - specifiying ``--runxfail`` on command line virtually ignores xfail markers - -- expose (previously internal) commonly useful methods: - py.io.get_terminal_with() -> return terminal width - py.io.ansi_print(...) -> print colored/bold text on linux/win32 - py.io.saferepr(obj) -> return limited representation string - -- expose test outcome related exceptions as py.test.skip.Exception, - py.test.raises.Exception etc., useful mostly for plugins - doing special outcome interpretation/tweaking - -- (issue85) fix junitxml plugin to handle tests with non-ascii output - -- fix/refine python3 compatibility (thanks Benjamin Peterson) - -- fixes for making the jython/win32 combination work, note however: - jython2.5.1/win32 does not provide a command line launcher, see - http://bugs.jython.org/issue1491 . See pylib install documentation - for how to work around. - -- fixes for handling of unicode exception values and unprintable objects - -- (issue87) fix unboundlocal error in assertionold code - -- (issue86) improve documentation for looponfailing - -- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method - -- ship distribute_setup.py version 0.6.10 - -- added links to the new capturelog and coverage plugins - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links - -Changes between 1.2 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location - -Changes between 1.1.0 and 1.0.2 -===================================== - -* adjust and improve docs - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation - -Changes between 1.0.1 and 1.0.2 -===================================== - -* fixing packaging issues, triggered by fedora redhat packaging, - also added doc, examples and contrib dirs to the tarball. - -* added a documentation link to the new django plugin. - -Changes between 1.0.0 and 1.0.1 -===================================== - -* added a 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* capturing of unicode writes or encoded strings to sys.stdout/err - work better, also terminalwriting was adapted and somewhat - unified between windows and linux. - -* improved documentation layout and content a lot - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames - -Changes between 1.0.0b9 and 1.0.0 -===================================== - -* more terse reporting try to show filesystem path relatively to current dir -* improve xfail output a bit - -Changes between 1.0.0b8 and 1.0.0b9 -===================================== - -* cleanly handle and report final teardown of test setup - -* fix svn-1.6 compat issue with py.path.svnwc().versioned() - (thanks Wouter Vanden Hove) - -* setup/teardown or collection problems now show as ERRORs - or with big "E"'s in the progress lines. they are reported - and counted separately. - -* dist-testing: properly handle test items that get locally - collected but cannot be collected on the remote side - often - due to platform/dependency reasons - -* simplified py.test.mark API - see keyword plugin documentation - -* integrate better with logging: capturing now by default captures - test functions and their immediate setup/teardown in a single stream - -* capsys and capfd funcargs now have a readouterr() and a close() method - (underlyingly py.io.StdCapture/FD objects are used which grew a - readouterr() method as well to return snapshots of captured out/err) - -* make assert-reinterpretation work better with comparisons not - returning bools (reported with numpy from thanks maciej fijalkowski) - -* reworked per-test output capturing into the pytest_iocapture.py plugin - and thus removed capturing code from config object - -* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) - - -Changes between 1.0.0b7 and 1.0.0b8 -===================================== - -* pytest_unittest-plugin is now enabled by default - -* introduced pytest_keyboardinterrupt hook and - refined pytest_sessionfinish hooked, added tests. - -* workaround a buggy logging module interaction ("closing already closed - files"). Thanks to Sridhar Ratnakumar for triggering. - -* if plugins use "py.test.importorskip" for importing - a dependency only a warning will be issued instead - of exiting the testing process. - -* many improvements to docs: - - refined funcargs doc , use the term "factory" instead of "provider" - - added a new talk/tutorial doc page - - better download page - - better plugin docstrings - - added new plugins page and automatic doc generation script - -* fixed teardown problem related to partially failing funcarg setups - (thanks MrTopf for reporting), "pytest_runtest_teardown" is now - always invoked even if the "pytest_runtest_setup" failed. - -* tweaked doctest output for docstrings in py modules, - thanks Radomir. - -Changes between 1.0.0b3 and 1.0.0b7 -============================================= - -* renamed py.test.xfail back to py.test.mark.xfail to avoid - two ways to decorate for xfail - -* re-added py.test.mark decorator for setting keywords on functions - (it was actually documented so removing it was not nice) - -* remove scope-argument from request.addfinalizer() because - request.cached_setup has the scope arg. TOOWTDI. - -* perform setup finalization before reporting failures - -* apply modified patches from Andreas Kloeckner to allow - test functions to have no func_code (#22) and to make - "-k" and function keywords work (#20) - -* apply patch from Daniel Peolzleithner (issue #23) - -* resolve issue #18, multiprocessing.Manager() and - redirection clash - -* make __name__ == "__channelexec__" for remote_exec code - -Changes between 1.0.0b1 and 1.0.0b3 -============================================= - -* plugin classes are removed: one now defines - hooks directly in conftest.py or global pytest_*.py - files. - -* added new pytest_namespace(config) hook that allows - to inject helpers directly to the py.test.* namespace. - -* documented and refined many hooks - -* added new style of generative tests via - pytest_generate_tests hook that integrates - well with function arguments. - - -Changes between 0.9.2 and 1.0.0b1 -============================================= - -* introduced new "funcarg" setup method, - see doc/test/funcarg.txt - -* introduced plugin architecuture and many - new py.test plugins, see - doc/test/plugins.txt - -* teardown_method is now guaranteed to get - called after a test method has run. - -* new method: py.test.importorskip(mod,minversion) - will either import or call py.test.skip() - -* completely revised internal py.test architecture - -* new py.process.ForkedFunc object allowing to - fork execution of a function to a sub process - and getting a result back. - -XXX lots of things missing here XXX - -Changes between 0.9.1 and 0.9.2 -=============================== - -* refined installation and metadata, created new setup.py, - now based on setuptools/ez_setup (thanks to Ralf Schmitt - for his support). - -* improved the way of making py.* scripts available in - windows environments, they are now added to the - Scripts directory as ".cmd" files. - -* py.path.svnwc.status() now is more complete and - uses xml output from the 'svn' command if available - (Guido Wesdorp) - -* fix for py.path.svn* to work with svn 1.5 - (Chris Lamb) - -* fix path.relto(otherpath) method on windows to - use normcase for checking if a path is relative. - -* py.test's traceback is better parseable from editors - (follows the filenames:LINENO: MSG convention) - (thanks to Osmo Salomaa) - -* fix to javascript-generation, "py.test --runbrowser" - should work more reliably now - -* removed previously accidentally added - py.test.broken and py.test.notimplemented helpers. - -* there now is a py.__version__ attribute - -Changes between 0.9.0 and 0.9.1 -=============================== - -This is a fairly complete list of changes between 0.9 and 0.9.1, which can -serve as a reference for developers. - -* allowing + signs in py.path.svn urls [39106] -* fixed support for Failed exceptions without excinfo in py.test [39340] -* added support for killing processes for Windows (as well as platforms that - support os.kill) in py.misc.killproc [39655] -* added setup/teardown for generative tests to py.test [40702] -* added detection of FAILED TO LOAD MODULE to py.test [40703, 40738, 40739] -* fixed problem with calling .remove() on wcpaths of non-versioned files in - py.path [44248] -* fixed some import and inheritance issues in py.test [41480, 44648, 44655] -* fail to run greenlet tests when pypy is available, but without stackless - [45294] -* small fixes in rsession tests [45295] -* fixed issue with 2.5 type representations in py.test [45483, 45484] -* made that internal reporting issues displaying is done atomically in py.test - [45518] -* made that non-existing files are igored by the py.lookup script [45519] -* improved exception name creation in py.test [45535] -* made that less threads are used in execnet [merge in 45539] -* removed lock required for atomical reporting issue displaying in py.test - [45545] -* removed globals from execnet [45541, 45547] -* refactored cleanup mechanics, made that setDaemon is set to 1 to make atexit - get called in 2.5 (py.execnet) [45548] -* fixed bug in joining threads in py.execnet's servemain [45549] -* refactored py.test.rsession tests to not rely on exact output format anymore - [45646] -* using repr() on test outcome [45647] -* added 'Reason' classes for py.test.skip() [45648, 45649] -* killed some unnecessary sanity check in py.test.collect [45655] -* avoid using os.tmpfile() in py.io.fdcapture because on Windows it's only - usable by Administrators [45901] -* added support for locking and non-recursive commits to py.path.svnwc [45994] -* locking files in py.execnet to prevent CPython from segfaulting [46010] -* added export() method to py.path.svnurl -* fixed -d -x in py.test [47277] -* fixed argument concatenation problem in py.path.svnwc [49423] -* restore py.test behaviour that it exits with code 1 when there are failures - [49974] -* don't fail on html files that don't have an accompanying .txt file [50606] -* fixed 'utestconvert.py < input' [50645] -* small fix for code indentation in py.code.source [50755] -* fix _docgen.py documentation building [51285] -* improved checks for source representation of code blocks in py.test [51292] -* added support for passing authentication to py.path.svn* objects [52000, - 52001] -* removed sorted() call for py.apigen tests in favour of [].sort() to support - Python 2.3 [52481] diff --git a/tests/wpt/tests/tools/third_party/py/LICENSE b/tests/wpt/tests/tools/third_party/py/LICENSE deleted file mode 100644 index 31ecdfb1dbc..00000000000 --- a/tests/wpt/tests/tools/third_party/py/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - diff --git a/tests/wpt/tests/tools/third_party/py/MANIFEST.in b/tests/wpt/tests/tools/third_party/py/MANIFEST.in deleted file mode 100644 index 6d255b1a9e5..00000000000 --- a/tests/wpt/tests/tools/third_party/py/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include CHANGELOG.rst -include AUTHORS -include README.rst -include setup.py -include LICENSE -include conftest.py -include tox.ini -recursive-include py *.pyi -graft doc -graft testing -global-exclude *.pyc diff --git a/tests/wpt/tests/tools/third_party/py/README.rst b/tests/wpt/tests/tools/third_party/py/README.rst deleted file mode 100644 index 80800b2b7ae..00000000000 --- a/tests/wpt/tests/tools/third_party/py/README.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. image:: https://img.shields.io/pypi/v/py.svg - :target: https://pypi.org/project/py - -.. image:: https://img.shields.io/conda/vn/conda-forge/py.svg - :target: https://anaconda.org/conda-forge/py - -.. image:: https://img.shields.io/pypi/pyversions/py.svg - :target: https://pypi.org/project/py - -.. image:: https://github.com/pytest-dev/py/workflows/build/badge.svg - :target: https://github.com/pytest-dev/py/actions - - -**NOTE**: this library is in **maintenance mode** and should not be used in new code. - -The py lib is a Python development support library featuring -the following tools and modules: - -* ``py.path``: uniform local and svn path objects -> please use pathlib/pathlib2 instead -* ``py.apipkg``: explicit API control and lazy-importing -> please use the standalone package instead -* ``py.iniconfig``: easy parsing of .ini files -> please use the standalone package instead -* ``py.code``: dynamic code generation and introspection (deprecated, moved to ``pytest`` as a implementation detail). - -**NOTE**: prior to the 1.4 release this distribution used to -contain py.test which is now its own package, see https://docs.pytest.org - -For questions and more information please visit https://py.readthedocs.io - -Bugs and issues: https://github.com/pytest-dev/py - -Authors: Holger Krekel and others, 2004-2017 diff --git a/tests/wpt/tests/tools/third_party/py/RELEASING.rst b/tests/wpt/tests/tools/third_party/py/RELEASING.rst deleted file mode 100644 index fb588e3ab74..00000000000 --- a/tests/wpt/tests/tools/third_party/py/RELEASING.rst +++ /dev/null @@ -1,17 +0,0 @@ -Release Procedure ------------------ - -#. Create a branch ``release-X.Y.Z`` from the latest ``master``. - -#. Manually update the ``CHANGELOG.rst`` and commit. - -#. Open a PR for this branch targeting ``master``. - -#. After all tests pass and the PR has been approved by at least another maintainer, publish to PyPI by creating and pushing a tag:: - - git tag X.Y.Z - git push git@github.com:pytest-dev/py X.Y.Z - - Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/py>`_. - -#. Merge your PR to ``master``. diff --git a/tests/wpt/tests/tools/third_party/py/bench/localpath.py b/tests/wpt/tests/tools/third_party/py/bench/localpath.py deleted file mode 100644 index aad44f2e669..00000000000 --- a/tests/wpt/tests/tools/third_party/py/bench/localpath.py +++ /dev/null @@ -1,73 +0,0 @@ -import py - -class Listdir: - numiter = 100000 - numentries = 100 - - def setup(self): - tmpdir = py.path.local.make_numbered_dir(self.__class__.__name__) - for i in range(self.numentries): - tmpdir.join(str(i)) - self.tmpdir = tmpdir - - def run(self): - return self.tmpdir.listdir() - -class Listdir_arg(Listdir): - numiter = 100000 - numentries = 100 - - def run(self): - return self.tmpdir.listdir("47") - -class Join_onearg(Listdir): - def run(self): - self.tmpdir.join("17") - self.tmpdir.join("18") - self.tmpdir.join("19") - -class Join_multi(Listdir): - def run(self): - self.tmpdir.join("a", "b") - self.tmpdir.join("a", "b", "c") - self.tmpdir.join("a", "b", "c", "d") - -class Check(Listdir): - def run(self): - self.tmpdir.check() - self.tmpdir.check() - self.tmpdir.check() - -class CheckDir(Listdir): - def run(self): - self.tmpdir.check(dir=1) - self.tmpdir.check(dir=1) - assert not self.tmpdir.check(dir=0) - -class CheckDir2(Listdir): - def run(self): - self.tmpdir.stat().isdir() - self.tmpdir.stat().isdir() - assert self.tmpdir.stat().isdir() - -class CheckFile(Listdir): - def run(self): - self.tmpdir.check(file=1) - assert not self.tmpdir.check(file=1) - assert self.tmpdir.check(file=0) - -if __name__ == "__main__": - import time - for cls in [Listdir, Listdir_arg, - Join_onearg, Join_multi, - Check, CheckDir, CheckDir2, CheckFile,]: - - inst = cls() - inst.setup() - now = time.time() - for i in xrange(cls.numiter): - inst.run() - elapsed = time.time() - now - print("%s: %d loops took %.2f seconds, per call %.6f" %( - cls.__name__, - cls.numiter, elapsed, elapsed / cls.numiter)) diff --git a/tests/wpt/tests/tools/third_party/py/codecov.yml b/tests/wpt/tests/tools/third_party/py/codecov.yml deleted file mode 100644 index a0a308588e2..00000000000 --- a/tests/wpt/tests/tools/third_party/py/codecov.yml +++ /dev/null @@ -1,7 +0,0 @@ -coverage: - status: - project: true - patch: true - changes: true - -comment: off diff --git a/tests/wpt/tests/tools/third_party/py/conftest.py b/tests/wpt/tests/tools/third_party/py/conftest.py deleted file mode 100644 index 5bff3fe0224..00000000000 --- a/tests/wpt/tests/tools/third_party/py/conftest.py +++ /dev/null @@ -1,60 +0,0 @@ -import py -import pytest -import sys - -pytest_plugins = 'doctest', 'pytester' - -collect_ignore = ['build', 'doc/_build'] - - -def pytest_addoption(parser): - group = parser.getgroup("pylib", "py lib testing options") - group.addoption('--runslowtests', - action="store_true", dest="runslowtests", default=False, - help=("run slow tests")) - -@pytest.fixture -def sshhost(request): - val = request.config.getvalue("sshhost") - if val: - return val - py.test.skip("need --sshhost option") - - -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - 'python2.7': r'C:\Python27\python.exe', -} - - -def getexecutable(name, cache={}): - try: - return cache[name] - except KeyError: - executable = py.path.local.sysfind(name) - if executable: - if name == "jython": - import subprocess - popen = subprocess.Popen( - [str(executable), "--version"], - universal_newlines=True, stderr=subprocess.PIPE) - out, err = popen.communicate() - if not err or "2.5" not in err: - executable = None - cache[name] = executable - return executable - - -@pytest.fixture(params=('python2.7', 'pypy-c', 'jython')) -def anypython(request): - name = request.param - executable = getexecutable(name) - if executable is None: - if sys.platform == "win32": - executable = winpymap.get(name, None) - if executable: - executable = py.path.local(executable) - if executable.check(): - return executable - py.test.skip("no %s found" % (name,)) - return executable diff --git a/tests/wpt/tests/tools/third_party/py/doc/Makefile b/tests/wpt/tests/tools/third_party/py/doc/Makefile deleted file mode 100644 index 0a0e89e01fe..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/Makefile +++ /dev/null @@ -1,133 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make <target>' where <target> is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -install: clean html - rsync -avz _build/html/ code:www-pylib/ - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/py.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/py.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/py" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/py" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/tests/wpt/tests/tools/third_party/py/doc/_templates/layout.html b/tests/wpt/tests/tools/third_party/py/doc/_templates/layout.html deleted file mode 100644 index 683863aa460..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/_templates/layout.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "!layout.html" %} - -{% block footer %} -{{ super() }} -<script type="text/javascript"> - - var _gaq = _gaq || []; - _gaq.push(['_setAccount', 'UA-7597274-14']); - _gaq.push(['_trackPageview']); - - (function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); - })(); - -</script> -{% endblock %} diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.0.txt deleted file mode 100644 index 07109313543..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.0.txt +++ /dev/null @@ -1,7 +0,0 @@ -py lib 1.0.0: XXX -====================================================================== - -Welcome to the 1.0.0 py lib release - a library aiming to -support agile and test-driven python development on various levels. - -XXX diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.2.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.2.txt deleted file mode 100644 index 8340dc44557..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-0.9.2.txt +++ /dev/null @@ -1,27 +0,0 @@ -py lib 0.9.2: bugfix release -============================= - -Welcome to the 0.9.2 py lib and py.test release - -mainly fixing Windows issues, providing better -packaging and integration with setuptools. - -Here is a quick summary of what the py lib provides: - -* py.test: cross-project testing tool with many advanced features -* py.execnet: ad-hoc code distribution to SSH, Socket and local sub processes -* py.magic.greenlet: micro-threads on standard CPython ("stackless-light") -* py.path: path abstractions over local and subversion files -* rich documentation of py's exported API -* tested against Linux, Win32, OSX, works on python 2.3-2.6 - -See here for more information: - -Pypi pages: https://pypi.org/project/py/ - -Download/Install: http://codespeak.net/py/0.9.2/download.html - -Documentation/API: http://codespeak.net/py/0.9.2/index.html - -best and have fun, - -holger krekel diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.0.txt deleted file mode 100644 index aef25ec239b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.0.txt +++ /dev/null @@ -1,63 +0,0 @@ - -pylib 1.0.0 released: testing-with-python innovations continue --------------------------------------------------------------------- - -Took a few betas but finally i uploaded a `1.0.0 py lib release`_, -featuring the mature and powerful py.test tool and "execnet-style" -*elastic* distributed programming. With the new release, there are -many new advanced automated testing features - here is a quick summary: - -* funcargs_ - pythonic zero-boilerplate fixtures for Python test functions : - - - totally separates test code, test configuration and test setup - - ideal for integration and functional tests - - allows for flexible and natural test parametrization schemes - -* new `plugin architecture`_, allowing easy-to-write project-specific and cross-project single-file plugins. The most notable new external plugin is `oejskit`_ which naturally enables **running and reporting of javascript-unittests in real-life browsers**. - -* many new features done in easy-to-improve `default plugins`_, highlights: - - * xfail: mark tests as "expected to fail" and report separately. - * pastebin: automatically send tracebacks to pocoo paste service - * capture: flexibly capture stdout/stderr of subprocesses, per-test ... - * monkeypatch: safely monkeypatch modules/classes from within tests - * unittest: run and integrate traditional unittest.py tests - * figleaf: generate html coverage reports with the figleaf module - * resultlog: generate buildbot-friendly reporting output - * ... - -* `distributed testing`_ and `elastic distributed execution`_: - - - new unified "TX" URL scheme for specifying remote processes - - new distribution modes "--dist=each" and "--dist=load" - - new sync/async ways to handle 1:N communication - - improved documentation - -The py lib continues to offer most of the functionality used by -the testing tool in `independent namespaces`_. - -Some non-test related code, notably greenlets/co-routines and -api-generation now live as their own projects which simplifies the -installation procedure because no C-Extensions are required anymore. - -The whole package should work well with Linux, Win32 and OSX, on Python -2.3, 2.4, 2.5 and 2.6. (Expect Python3 compatibility soon!) - -For more info, see the py.test and py lib documentation: - - http://pytest.org - - http://pylib.org - -have fun, -holger - -.. _`independent namespaces`: http://pylib.org -.. _`funcargs`: http://codespeak.net/py/dist/test/funcargs.html -.. _`plugin architecture`: http://codespeak.net/py/dist/test/extend.html -.. _`default plugins`: http://codespeak.net/py/dist/test/plugin/index.html -.. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html -.. _`elastic distributed execution`: http://codespeak.net/py/dist/execnet.html -.. _`1.0.0 py lib release`: https://pypi.org/project/py/ -.. _`oejskit`: http://codespeak.net/py/dist/test/plugin/oejskit.html - diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.1.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.1.txt deleted file mode 100644 index 0c9f8760bdb..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.1.txt +++ /dev/null @@ -1,48 +0,0 @@ -1.0.1: improved reporting, nose/unittest.py support, bug fixes ------------------------------------------------------------------------ - -This is a bugfix release of pylib/py.test also coming with: - -* improved documentation, improved navigation -* test failure reporting improvements -* support for directly running existing nose/unittest.py style tests - -visit here for more info, including quickstart and tutorials: - - http://pytest.org and http://pylib.org - - -Changelog 1.0.0 to 1.0.1 ------------------------- - -* added a default 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* improved documentation, better navigation: see http://pytest.org - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* unicode fixes: capturing and unicode writes to sys.stdout - (through e.g a print statement) now work within tests, - they are encoded as "utf8" by default, also terminalwriting - was adapted and somewhat unified between windows and linux - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.2.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.2.txt deleted file mode 100644 index 23546195353..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.0.2.txt +++ /dev/null @@ -1,5 +0,0 @@ -1.0.2: packaging fixes ------------------------------------------------------------------------ - -this release is purely a release for fixing packaging issues. - diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.0.txt deleted file mode 100644 index 0441c3215eb..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.0.txt +++ /dev/null @@ -1,115 +0,0 @@ -py.test/pylib 1.1.0: Python3, Jython, advanced skipping, cleanups ... --------------------------------------------------------------------------------- - -Features: - -* compatible to Python3 (single py2/py3 source), `easy to install`_ -* conditional skipping_: skip/xfail based on platform/dependencies -* generalized marking_: mark tests one a whole-class or whole-module basis - -Fixes: - -* code reduction and "de-magification" (e.g. 23 KLoc -> 11 KLOC) -* distribute testing requires the now separately released execnet_ package -* funcarg-setup/caching, "same-name" test modules now cause an exlicit error -* de-cluttered reporting options, --report for skipped/xfail details - -Compatibilities - -1.1.0 should allow running test code that already worked well with 1.0.2 -plus some more due to improved unittest/nose compatibility. - -More information: http://pytest.org - -thanks and have fun, - -holger (http://twitter.com/hpk42) - -.. _execnet: http://codespeak.net/execnet -.. _`easy to install`: ../install.html -.. _marking: ../test/plugin/mark.html -.. _skipping: ../test/plugin/skipping.html - - -Changelog 1.0.2 -> 1.1.0 ------------------------------------------------------------------------ - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.1.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.1.txt deleted file mode 100644 index 83e6a1fd8d9..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.1.1.txt +++ /dev/null @@ -1,48 +0,0 @@ -py.test/pylib 1.1.1: bugfix release, setuptools plugin registration --------------------------------------------------------------------------------- - -This is a compatibility fixing release of pylib/py.test to work -better with previous 1.0.x test code bases. It also contains fixes -and changes to work with `execnet>=1.0.0`_ to provide distributed -testing and looponfailing testing modes. py-1.1.1 also introduces -a new mechanism for registering plugins via setuptools. - -What is pylib/py.test? ------------------------ - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has an extensive plugin architecture and can run many -existing common Python test suites without modification. Moreover, -it offers some unique features not found in other -testing tools. See http://pytest.org for more info. - -The pylib also contains a localpath and svnpath implementation -and some developer-oriented command line tools. See -http://pylib.org for more info. - -thanks to all who helped and gave feedback, -have fun, - -holger (http://twitter.com/hpk42) - -.. _`execnet>=1.0.0`: http://codespeak.net/execnet - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.0.txt deleted file mode 100644 index 4f6a5614476..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.0.txt +++ /dev/null @@ -1,116 +0,0 @@ -py.test/pylib 1.2.0: junitxml, standalone test scripts, pluginization --------------------------------------------------------------------------------- - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has a simple plugin architecture and can run many -existing common Python test suites without modification. It offers -some unique features not found in other testing tools. -See http://pytest.org for more info. - -py.test 1.2.0 brings many bug fixes and interesting new abilities: - -* --junitxml=path will create an XML file for use with CI processing -* --genscript=path creates a standalone py.test-equivalent test-script -* --ignore=path prevents collection of anything below that path -* --confcutdir=path only lookup conftest.py test configs below that path -* a 'pytest_report_header' hook to add info to the terminal report header -* a 'pytestconfig' function argument gives direct access to option values -* 'pytest_generate_tests' can now be put into a class as well -* on CPython py.test additionally installs as "py.test-VERSION", on - Jython as py.test-jython and on PyPy as py.test-pypy-XYZ - -Apart from many bug fixes 1.2.0 also has better pluginization: -Distributed testing and looponfailing testing now live in the -separately installable 'pytest-xdist' plugin. The same is true for -'pytest-figleaf' for doing coverage reporting. Those two plugins -can serve well now as blue prints for doing your own. - -thanks to all who helped and gave feedback, -have fun, - -holger krekel, January 2010 - -Changes between 1.2.0 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.1.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.1.txt deleted file mode 100644 index 5bf8ba22dc6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.2.1.txt +++ /dev/null @@ -1,66 +0,0 @@ -py.test/pylib 1.2.1: little fixes and improvements --------------------------------------------------------------------------------- - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has a simple plugin architecture and can run many -existing common Python test suites without modification. It offers -some unique features not found in other testing tools. -See http://pytest.org for more info. - -py.test 1.2.1 brings bug fixes and some new options and abilities triggered -by user feedback: - -* --funcargs [testpath] will show available builtin- and project funcargs. -* display a short and concise traceback if funcarg lookup fails. -* early-load "conftest.py" files in non-dot first-level sub directories. -* --tb=line will print a single line for each failing test (issue67) -* py.cleanup has a number of new options, cleanups up setup.py related files -* fix issue78: always call python-level teardown functions even if the - according setup failed. - -For more detailed information see the changelog below. - -cheers and have fun, - -holger - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.0.txt deleted file mode 100644 index cf97db0367a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.0.txt +++ /dev/null @@ -1,580 +0,0 @@ -py.test/pylib 1.3.0: new options, per-plugin hooks, fixes ... -=========================================================================== - -The 1.3.0 release introduces new options, bug fixes and improved compatibility -with Python3 and Jython-2.5.1 on Windows. If you already use py-1.2 chances -are you can use py-1.3.0. See the below CHANGELOG for more details and -http://pylib.org/install.html for installation instructions. - -py.test is an advanced automated testing tool working with Python2, -Python3, Jython and PyPy versions on all major operating systems. It -offers a no-boilerplate testing approach and has inspired other testing -tools and enhancements in the standard Python library for more than five -years. It has a simple and extensive plugin architecture, configurable -reporting and provides unique ways to make it fit to your testing -process and needs. - -See http://pytest.org for more info. - -cheers and have fun, - -holger krekel - -Changes between 1.2.1 and 1.3.0 -================================================== - -- deprecate --report option in favour of a new shorter and easier to - remember -r option: it takes a string argument consisting of any - combination of 'xfsX' characters. They relate to the single chars - you see during the dotted progress printing and will print an extra line - per test at the end of the test run. This extra line indicates the exact - position or test ID that you directly paste to the py.test cmdline in order - to re-run a particular test. - -- allow external plugins to register new hooks via the new - pytest_addhooks(pluginmanager) hook. The new release of - the pytest-xdist plugin for distributed and looponfailing - testing requires this feature. - -- add a new pytest_ignore_collect(path, config) hook to allow projects and - plugins to define exclusion behaviour for their directory structure - - for example you may define in a conftest.py this method:: - - def pytest_ignore_collect(path): - return path.check(link=1) - - to prevent even collection of any tests in symlinked dirs. - -- new pytest_pycollect_makemodule(path, parent) hook for - allowing customization of the Module collection object for a - matching test module. - -- extend and refine xfail mechanism:: - - @py.test.mark.xfail(run=False) do not run the decorated test - @py.test.mark.xfail(reason="...") prints the reason string in xfail summaries - - specifiying ``--runxfail`` on command line ignores xfail markers to show - you the underlying traceback. - -- expose (previously internal) commonly useful methods: - py.io.get_terminal_with() -> return terminal width - py.io.ansi_print(...) -> print colored/bold text on linux/win32 - py.io.saferepr(obj) -> return limited representation string - -- expose test outcome related exceptions as py.test.skip.Exception, - py.test.raises.Exception etc., useful mostly for plugins - doing special outcome interpretation/tweaking - -- (issue85) fix junitxml plugin to handle tests with non-ascii output - -- fix/refine python3 compatibility (thanks Benjamin Peterson) - -- fixes for making the jython/win32 combination work, note however: - jython2.5.1/win32 does not provide a command line launcher, see - http://bugs.jython.org/issue1491 . See pylib install documentation - for how to work around. - -- fixes for handling of unicode exception values and unprintable objects - -- (issue87) fix unboundlocal error in assertionold code - -- (issue86) improve documentation for looponfailing - -- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method - -- ship distribute_setup.py version 0.6.10 - -- added links to the new capturelog and coverage plugins - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links - -Changes between 1.2 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location - -Changes between 1.1.0 and 1.0.2 -===================================== - -* adjust and improve docs - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation - -Changes between 1.0.1 and 1.0.2 -===================================== - -* fixing packaging issues, triggered by fedora redhat packaging, - also added doc, examples and contrib dirs to the tarball. - -* added a documentation link to the new django plugin. - -Changes between 1.0.0 and 1.0.1 -===================================== - -* added a 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* capturing of unicode writes or encoded strings to sys.stdout/err - work better, also terminalwriting was adapted and somewhat - unified between windows and linux. - -* improved documentation layout and content a lot - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames - -Changes between 1.0.0b9 and 1.0.0 -===================================== - -* more terse reporting try to show filesystem path relatively to current dir -* improve xfail output a bit - -Changes between 1.0.0b8 and 1.0.0b9 -===================================== - -* cleanly handle and report final teardown of test setup - -* fix svn-1.6 compat issue with py.path.svnwc().versioned() - (thanks Wouter Vanden Hove) - -* setup/teardown or collection problems now show as ERRORs - or with big "E"'s in the progress lines. they are reported - and counted separately. - -* dist-testing: properly handle test items that get locally - collected but cannot be collected on the remote side - often - due to platform/dependency reasons - -* simplified py.test.mark API - see keyword plugin documentation - -* integrate better with logging: capturing now by default captures - test functions and their immediate setup/teardown in a single stream - -* capsys and capfd funcargs now have a readouterr() and a close() method - (underlyingly py.io.StdCapture/FD objects are used which grew a - readouterr() method as well to return snapshots of captured out/err) - -* make assert-reinterpretation work better with comparisons not - returning bools (reported with numpy from thanks maciej fijalkowski) - -* reworked per-test output capturing into the pytest_iocapture.py plugin - and thus removed capturing code from config object - -* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) - - -Changes between 1.0.0b7 and 1.0.0b8 -===================================== - -* pytest_unittest-plugin is now enabled by default - -* introduced pytest_keyboardinterrupt hook and - refined pytest_sessionfinish hooked, added tests. - -* workaround a buggy logging module interaction ("closing already closed - files"). Thanks to Sridhar Ratnakumar for triggering. - -* if plugins use "py.test.importorskip" for importing - a dependency only a warning will be issued instead - of exiting the testing process. - -* many improvements to docs: - - refined funcargs doc , use the term "factory" instead of "provider" - - added a new talk/tutorial doc page - - better download page - - better plugin docstrings - - added new plugins page and automatic doc generation script - -* fixed teardown problem related to partially failing funcarg setups - (thanks MrTopf for reporting), "pytest_runtest_teardown" is now - always invoked even if the "pytest_runtest_setup" failed. - -* tweaked doctest output for docstrings in py modules, - thanks Radomir. - -Changes between 1.0.0b3 and 1.0.0b7 -============================================= - -* renamed py.test.xfail back to py.test.mark.xfail to avoid - two ways to decorate for xfail - -* re-added py.test.mark decorator for setting keywords on functions - (it was actually documented so removing it was not nice) - -* remove scope-argument from request.addfinalizer() because - request.cached_setup has the scope arg. TOOWTDI. - -* perform setup finalization before reporting failures - -* apply modified patches from Andreas Kloeckner to allow - test functions to have no func_code (#22) and to make - "-k" and function keywords work (#20) - -* apply patch from Daniel Peolzleithner (issue #23) - -* resolve issue #18, multiprocessing.Manager() and - redirection clash - -* make __name__ == "__channelexec__" for remote_exec code - -Changes between 1.0.0b1 and 1.0.0b3 -============================================= - -* plugin classes are removed: one now defines - hooks directly in conftest.py or global pytest_*.py - files. - -* added new pytest_namespace(config) hook that allows - to inject helpers directly to the py.test.* namespace. - -* documented and refined many hooks - -* added new style of generative tests via - pytest_generate_tests hook that integrates - well with function arguments. - - -Changes between 0.9.2 and 1.0.0b1 -============================================= - -* introduced new "funcarg" setup method, - see doc/test/funcarg.txt - -* introduced plugin architecuture and many - new py.test plugins, see - doc/test/plugins.txt - -* teardown_method is now guaranteed to get - called after a test method has run. - -* new method: py.test.importorskip(mod,minversion) - will either import or call py.test.skip() - -* completely revised internal py.test architecture - -* new py.process.ForkedFunc object allowing to - fork execution of a function to a sub process - and getting a result back. - -XXX lots of things missing here XXX - -Changes between 0.9.1 and 0.9.2 -=============================== - -* refined installation and metadata, created new setup.py, - now based on setuptools/ez_setup (thanks to Ralf Schmitt - for his support). - -* improved the way of making py.* scripts available in - windows environments, they are now added to the - Scripts directory as ".cmd" files. - -* py.path.svnwc.status() now is more complete and - uses xml output from the 'svn' command if available - (Guido Wesdorp) - -* fix for py.path.svn* to work with svn 1.5 - (Chris Lamb) - -* fix path.relto(otherpath) method on windows to - use normcase for checking if a path is relative. - -* py.test's traceback is better parseable from editors - (follows the filenames:LINENO: MSG convention) - (thanks to Osmo Salomaa) - -* fix to javascript-generation, "py.test --runbrowser" - should work more reliably now - -* removed previously accidentally added - py.test.broken and py.test.notimplemented helpers. - -* there now is a py.__version__ attribute - -Changes between 0.9.0 and 0.9.1 -=============================== - -This is a fairly complete list of changes between 0.9 and 0.9.1, which can -serve as a reference for developers. - -* allowing + signs in py.path.svn urls [39106] -* fixed support for Failed exceptions without excinfo in py.test [39340] -* added support for killing processes for Windows (as well as platforms that - support os.kill) in py.misc.killproc [39655] -* added setup/teardown for generative tests to py.test [40702] -* added detection of FAILED TO LOAD MODULE to py.test [40703, 40738, 40739] -* fixed problem with calling .remove() on wcpaths of non-versioned files in - py.path [44248] -* fixed some import and inheritance issues in py.test [41480, 44648, 44655] -* fail to run greenlet tests when pypy is available, but without stackless - [45294] -* small fixes in rsession tests [45295] -* fixed issue with 2.5 type representations in py.test [45483, 45484] -* made that internal reporting issues displaying is done atomically in py.test - [45518] -* made that non-existing files are igored by the py.lookup script [45519] -* improved exception name creation in py.test [45535] -* made that less threads are used in execnet [merge in 45539] -* removed lock required for atomical reporting issue displaying in py.test - [45545] -* removed globals from execnet [45541, 45547] -* refactored cleanup mechanics, made that setDaemon is set to 1 to make atexit - get called in 2.5 (py.execnet) [45548] -* fixed bug in joining threads in py.execnet's servemain [45549] -* refactored py.test.rsession tests to not rely on exact output format anymore - [45646] -* using repr() on test outcome [45647] -* added 'Reason' classes for py.test.skip() [45648, 45649] -* killed some unnecessary sanity check in py.test.collect [45655] -* avoid using os.tmpfile() in py.io.fdcapture because on Windows it's only - usable by Administrators [45901] -* added support for locking and non-recursive commits to py.path.svnwc [45994] -* locking files in py.execnet to prevent CPython from segfaulting [46010] -* added export() method to py.path.svnurl -* fixed -d -x in py.test [47277] -* fixed argument concatenation problem in py.path.svnwc [49423] -* restore py.test behaviour that it exits with code 1 when there are failures - [49974] -* don't fail on html files that don't have an accompanying .txt file [50606] -* fixed 'utestconvert.py < input' [50645] -* small fix for code indentation in py.code.source [50755] -* fix _docgen.py documentation building [51285] -* improved checks for source representation of code blocks in py.test [51292] -* added support for passing authentication to py.path.svn* objects [52000, - 52001] -* removed sorted() call for py.apigen tests in favour of [].sort() to support - Python 2.3 [52481] diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.1.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.1.txt deleted file mode 100644 index 471de408a10..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.1.txt +++ /dev/null @@ -1,104 +0,0 @@ -py.test/pylib 1.3.1: new py.test.xfail, --maxfail, better reporting -=========================================================================== - -The pylib/py.test 1.3.1 release brings: - -- the new imperative ``py.test.xfail()`` helper in order to have a test or - setup function result in an "expected failure" -- a new option ``--maxfail=NUM`` to stop the test run after some failures -- markers/decorators are now applicable to test classes (>=Python2.6) -- improved reporting, shorter tracebacks in several cases -- some simplified internals, more compatibility with Jython and PyPy -- bug fixes and various refinements - -See the below CHANGELOG entry below for more details and -http://pylib.org/install.html for installation instructions. - -If you used older versions of py.test you should be able to upgrade -to 1.3.1 without changes to your test source code. - -py.test is an automated testing tool working with Python2, -Python3, Jython and PyPy versions on all major operating systems. It -offers a no-boilerplate testing approach and has inspired other testing -tools and enhancements in the standard Python library for more than five -years. It has a simple and extensive plugin architecture, configurable -reporting and provides unique ways to make it fit to your testing -process and needs. - -See http://pytest.org for more info. - -cheers and have fun, - -holger krekel - -Changes between 1.3.0 and 1.3.1 -================================================== - -New features -++++++++++++++++++ - -- issue91: introduce new py.test.xfail(reason) helper - to imperatively mark a test as expected to fail. Can - be used from within setup and test functions. This is - useful especially for parametrized tests when certain - configurations are expected-to-fail. In this case the - declarative approach with the @py.test.mark.xfail cannot - be used as it would mark all configurations as xfail. - -- issue102: introduce new --maxfail=NUM option to stop - test runs after NUM failures. This is a generalization - of the '-x' or '--exitfirst' option which is now equivalent - to '--maxfail=1'. Both '-x' and '--maxfail' will - now also print a line near the end indicating the Interruption. - -- issue89: allow py.test.mark decorators to be used on classes - (class decorators were introduced with python2.6) and - also allow to have multiple markers applied at class/module level - by specifying a list. - -- improve and refine letter reporting in the progress bar: - . pass - f failed test - s skipped tests (reminder: use for dependency/platform mismatch only) - x xfailed test (test that was expected to fail) - X xpassed test (test that was expected to fail but passed) - - You can use any combination of 'fsxX' with the '-r' extended - reporting option. The xfail/xpass results will show up as - skipped tests in the junitxml output - which also fixes - issue99. - -- make py.test.cmdline.main() return the exitstatus instead of raising - SystemExit and also allow it to be called multiple times. This of - course requires that your application and tests are properly teared - down and don't have global state. - -Fixes / Maintenance -++++++++++++++++++++++ - -- improved traceback presentation: - - improved and unified reporting for "--tb=short" option - - Errors during test module imports are much shorter, (using --tb=short style) - - raises shows shorter more relevant tracebacks - - --fulltrace now more systematically makes traces longer / inhibits cutting - -- improve support for raises and other dynamically compiled code by - manipulating python's linecache.cache instead of the previous - rather hacky way of creating custom code objects. This makes - it seemlessly work on Jython and PyPy where it previously didn't. - -- fix issue96: make capturing more resilient against Control-C - interruptions (involved somewhat substantial refactoring - to the underlying capturing functionality to avoid race - conditions). - -- fix chaining of conditional skipif/xfail decorators - so it works now - as expected to use multiple @py.test.mark.skipif(condition) decorators, - including specific reporting which of the conditions lead to skipping. - -- fix issue95: late-import zlib so that it's not required - for general py.test startup. - -- fix issue94: make reporting more robust against bogus source code - (and internally be more careful when presenting unexpected byte sequences) - diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.2.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.2.txt deleted file mode 100644 index 599dfbed755..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.2.txt +++ /dev/null @@ -1,720 +0,0 @@ -py.test/pylib 1.3.2: API and reporting refinements, many fixes -=========================================================================== - -The pylib/py.test 1.3.2 release brings many bug fixes and some new -features. It was refined for and tested against the recently released -Python2.7 and remains compatibile to the usual armada of interpreters -(Python2.4 through to Python3.1.2, Jython and PyPy). Note that for using -distributed testing features you'll need to upgrade to the jointly released -pytest-xdist-1.4 because of some internal refactorings. - -See http://pytest.org for general documentation and below for -a detailed CHANGELOG. - -cheers & particular thanks to Benjamin Peterson, Ronny Pfannschmidt -and all issue and patch contributors, - -holger krekel - -Changes between 1.3.1 and 1.3.2 -================================================== - -New features -++++++++++++++++++ - -- fix issue103: introduce py.test.raises as context manager, examples:: - - with py.test.raises(ZeroDivisionError): - x = 0 - 1 / x - - with py.test.raises(RuntimeError) as excinfo: - call_something() - - # you may do extra checks on excinfo.value|type|traceback here - - (thanks Ronny Pfannschmidt) - -- Funcarg factories can now dynamically apply a marker to a - test invocation. This is for example useful if a factory - provides parameters to a test which are expected-to-fail:: - - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail(reason="flaky config")) - ... - - def test_function(arg): - ... - -- improved error reporting on collection and import errors. This makes - use of a more general mechanism, namely that for custom test item/collect - nodes ``node.repr_failure(excinfo)`` is now uniformly called so that you can - override it to return a string error representation of your choice - which is going to be reported as a (red) string. - -- introduce '--junitprefix=STR' option to prepend a prefix - to all reports in the junitxml file. - -Bug fixes / Maintenance -++++++++++++++++++++++++++ - -- make tests and the ``pytest_recwarn`` plugin in particular fully compatible - to Python2.7 (if you use the ``recwarn`` funcarg warnings will be enabled so that - you can properly check for their existence in a cross-python manner). -- refine --pdb: ignore xfailed tests, unify its TB-reporting and - don't display failures again at the end. -- fix assertion interpretation with the ** operator (thanks Benjamin Peterson) -- fix issue105 assignment on the same line as a failing assertion (thanks Benjamin Peterson) -- fix issue104 proper escaping for test names in junitxml plugin (thanks anonymous) -- fix issue57 -f|--looponfail to work with xpassing tests (thanks Ronny) -- fix issue92 collectonly reporter and --pastebin (thanks Benjamin Peterson) -- fix py.code.compile(source) to generate unique filenames -- fix assertion re-interp problems on PyPy, by defering code - compilation to the (overridable) Frame.eval class. (thanks Amaury Forgeot) -- fix py.path.local.pyimport() to work with directories -- streamline py.path.local.mkdtemp implementation and usage -- don't print empty lines when showing junitxml-filename -- add optional boolean ignore_errors parameter to py.path.local.remove -- fix terminal writing on win32/python2.4 -- py.process.cmdexec() now tries harder to return properly encoded unicode objects - on all python versions -- install plain py.test/py.which scripts also for Jython, this helps to - get canonical script paths in virtualenv situations -- make path.bestrelpath(path) return ".", note that when calling - X.bestrelpath the assumption is that X is a directory. -- make initial conftest discovery ignore "--" prefixed arguments -- fix resultlog plugin when used in an multicpu/multihost xdist situation - (thanks Jakub Gustak) -- perform distributed testing related reporting in the xdist-plugin - rather than having dist-related code in the generic py.test - distribution -- fix homedir detection on Windows -- ship distribute_setup.py version 0.6.13 - -Changes between 1.3.0 and 1.3.1 -================================================== - -New features -++++++++++++++++++ - -- issue91: introduce new py.test.xfail(reason) helper - to imperatively mark a test as expected to fail. Can - be used from within setup and test functions. This is - useful especially for parametrized tests when certain - configurations are expected-to-fail. In this case the - declarative approach with the @py.test.mark.xfail cannot - be used as it would mark all configurations as xfail. - -- issue102: introduce new --maxfail=NUM option to stop - test runs after NUM failures. This is a generalization - of the '-x' or '--exitfirst' option which is now equivalent - to '--maxfail=1'. Both '-x' and '--maxfail' will - now also print a line near the end indicating the Interruption. - -- issue89: allow py.test.mark decorators to be used on classes - (class decorators were introduced with python2.6) and - also allow to have multiple markers applied at class/module level - by specifying a list. - -- improve and refine letter reporting in the progress bar: - . pass - f failed test - s skipped tests (reminder: use for dependency/platform mismatch only) - x xfailed test (test that was expected to fail) - X xpassed test (test that was expected to fail but passed) - - You can use any combination of 'fsxX' with the '-r' extended - reporting option. The xfail/xpass results will show up as - skipped tests in the junitxml output - which also fixes - issue99. - -- make py.test.cmdline.main() return the exitstatus instead of raising - SystemExit and also allow it to be called multiple times. This of - course requires that your application and tests are properly teared - down and don't have global state. - -Fixes / Maintenance -++++++++++++++++++++++ - -- improved traceback presentation: - - improved and unified reporting for "--tb=short" option - - Errors during test module imports are much shorter, (using --tb=short style) - - raises shows shorter more relevant tracebacks - - --fulltrace now more systematically makes traces longer / inhibits cutting - -- improve support for raises and other dynamically compiled code by - manipulating python's linecache.cache instead of the previous - rather hacky way of creating custom code objects. This makes - it seemlessly work on Jython and PyPy where it previously didn't. - -- fix issue96: make capturing more resilient against Control-C - interruptions (involved somewhat substantial refactoring - to the underlying capturing functionality to avoid race - conditions). - -- fix chaining of conditional skipif/xfail decorators - so it works now - as expected to use multiple @py.test.mark.skipif(condition) decorators, - including specific reporting which of the conditions lead to skipping. - -- fix issue95: late-import zlib so that it's not required - for general py.test startup. - -- fix issue94: make reporting more robust against bogus source code - (and internally be more careful when presenting unexpected byte sequences) - - -Changes between 1.2.1 and 1.3.0 -================================================== - -- deprecate --report option in favour of a new shorter and easier to - remember -r option: it takes a string argument consisting of any - combination of 'xfsX' characters. They relate to the single chars - you see during the dotted progress printing and will print an extra line - per test at the end of the test run. This extra line indicates the exact - position or test ID that you directly paste to the py.test cmdline in order - to re-run a particular test. - -- allow external plugins to register new hooks via the new - pytest_addhooks(pluginmanager) hook. The new release of - the pytest-xdist plugin for distributed and looponfailing - testing requires this feature. - -- add a new pytest_ignore_collect(path, config) hook to allow projects and - plugins to define exclusion behaviour for their directory structure - - for example you may define in a conftest.py this method:: - - def pytest_ignore_collect(path): - return path.check(link=1) - - to prevent even a collection try of any tests in symlinked dirs. - -- new pytest_pycollect_makemodule(path, parent) hook for - allowing customization of the Module collection object for a - matching test module. - -- extend and refine xfail mechanism: - ``@py.test.mark.xfail(run=False)`` do not run the decorated test - ``@py.test.mark.xfail(reason="...")`` prints the reason string in xfail summaries - specifiying ``--runxfail`` on command line virtually ignores xfail markers - -- expose (previously internal) commonly useful methods: - py.io.get_terminal_with() -> return terminal width - py.io.ansi_print(...) -> print colored/bold text on linux/win32 - py.io.saferepr(obj) -> return limited representation string - -- expose test outcome related exceptions as py.test.skip.Exception, - py.test.raises.Exception etc., useful mostly for plugins - doing special outcome interpretation/tweaking - -- (issue85) fix junitxml plugin to handle tests with non-ascii output - -- fix/refine python3 compatibility (thanks Benjamin Peterson) - -- fixes for making the jython/win32 combination work, note however: - jython2.5.1/win32 does not provide a command line launcher, see - http://bugs.jython.org/issue1491 . See pylib install documentation - for how to work around. - -- fixes for handling of unicode exception values and unprintable objects - -- (issue87) fix unboundlocal error in assertionold code - -- (issue86) improve documentation for looponfailing - -- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method - -- ship distribute_setup.py version 0.6.10 - -- added links to the new capturelog and coverage plugins - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links - -Changes between 1.2 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location - -Changes between 1.1.0 and 1.0.2 -===================================== - -* adjust and improve docs - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation - -Changes between 1.0.1 and 1.0.2 -===================================== - -* fixing packaging issues, triggered by fedora redhat packaging, - also added doc, examples and contrib dirs to the tarball. - -* added a documentation link to the new django plugin. - -Changes between 1.0.0 and 1.0.1 -===================================== - -* added a 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* capturing of unicode writes or encoded strings to sys.stdout/err - work better, also terminalwriting was adapted and somewhat - unified between windows and linux. - -* improved documentation layout and content a lot - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames - -Changes between 1.0.0b9 and 1.0.0 -===================================== - -* more terse reporting try to show filesystem path relatively to current dir -* improve xfail output a bit - -Changes between 1.0.0b8 and 1.0.0b9 -===================================== - -* cleanly handle and report final teardown of test setup - -* fix svn-1.6 compat issue with py.path.svnwc().versioned() - (thanks Wouter Vanden Hove) - -* setup/teardown or collection problems now show as ERRORs - or with big "E"'s in the progress lines. they are reported - and counted separately. - -* dist-testing: properly handle test items that get locally - collected but cannot be collected on the remote side - often - due to platform/dependency reasons - -* simplified py.test.mark API - see keyword plugin documentation - -* integrate better with logging: capturing now by default captures - test functions and their immediate setup/teardown in a single stream - -* capsys and capfd funcargs now have a readouterr() and a close() method - (underlyingly py.io.StdCapture/FD objects are used which grew a - readouterr() method as well to return snapshots of captured out/err) - -* make assert-reinterpretation work better with comparisons not - returning bools (reported with numpy from thanks maciej fijalkowski) - -* reworked per-test output capturing into the pytest_iocapture.py plugin - and thus removed capturing code from config object - -* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) - - -Changes between 1.0.0b7 and 1.0.0b8 -===================================== - -* pytest_unittest-plugin is now enabled by default - -* introduced pytest_keyboardinterrupt hook and - refined pytest_sessionfinish hooked, added tests. - -* workaround a buggy logging module interaction ("closing already closed - files"). Thanks to Sridhar Ratnakumar for triggering. - -* if plugins use "py.test.importorskip" for importing - a dependency only a warning will be issued instead - of exiting the testing process. - -* many improvements to docs: - - refined funcargs doc , use the term "factory" instead of "provider" - - added a new talk/tutorial doc page - - better download page - - better plugin docstrings - - added new plugins page and automatic doc generation script - -* fixed teardown problem related to partially failing funcarg setups - (thanks MrTopf for reporting), "pytest_runtest_teardown" is now - always invoked even if the "pytest_runtest_setup" failed. - -* tweaked doctest output for docstrings in py modules, - thanks Radomir. - -Changes between 1.0.0b3 and 1.0.0b7 -============================================= - -* renamed py.test.xfail back to py.test.mark.xfail to avoid - two ways to decorate for xfail - -* re-added py.test.mark decorator for setting keywords on functions - (it was actually documented so removing it was not nice) - -* remove scope-argument from request.addfinalizer() because - request.cached_setup has the scope arg. TOOWTDI. - -* perform setup finalization before reporting failures - -* apply modified patches from Andreas Kloeckner to allow - test functions to have no func_code (#22) and to make - "-k" and function keywords work (#20) - -* apply patch from Daniel Peolzleithner (issue #23) - -* resolve issue #18, multiprocessing.Manager() and - redirection clash - -* make __name__ == "__channelexec__" for remote_exec code - -Changes between 1.0.0b1 and 1.0.0b3 -============================================= - -* plugin classes are removed: one now defines - hooks directly in conftest.py or global pytest_*.py - files. - -* added new pytest_namespace(config) hook that allows - to inject helpers directly to the py.test.* namespace. - -* documented and refined many hooks - -* added new style of generative tests via - pytest_generate_tests hook that integrates - well with function arguments. - - -Changes between 0.9.2 and 1.0.0b1 -============================================= - -* introduced new "funcarg" setup method, - see doc/test/funcarg.txt - -* introduced plugin architecuture and many - new py.test plugins, see - doc/test/plugins.txt - -* teardown_method is now guaranteed to get - called after a test method has run. - -* new method: py.test.importorskip(mod,minversion) - will either import or call py.test.skip() - -* completely revised internal py.test architecture - -* new py.process.ForkedFunc object allowing to - fork execution of a function to a sub process - and getting a result back. - -XXX lots of things missing here XXX - -Changes between 0.9.1 and 0.9.2 -=============================== - -* refined installation and metadata, created new setup.py, - now based on setuptools/ez_setup (thanks to Ralf Schmitt - for his support). - -* improved the way of making py.* scripts available in - windows environments, they are now added to the - Scripts directory as ".cmd" files. - -* py.path.svnwc.status() now is more complete and - uses xml output from the 'svn' command if available - (Guido Wesdorp) - -* fix for py.path.svn* to work with svn 1.5 - (Chris Lamb) - -* fix path.relto(otherpath) method on windows to - use normcase for checking if a path is relative. - -* py.test's traceback is better parseable from editors - (follows the filenames:LINENO: MSG convention) - (thanks to Osmo Salomaa) - -* fix to javascript-generation, "py.test --runbrowser" - should work more reliably now - -* removed previously accidentally added - py.test.broken and py.test.notimplemented helpers. - -* there now is a py.__version__ attribute - -Changes between 0.9.0 and 0.9.1 -=============================== - -This is a fairly complete list of changes between 0.9 and 0.9.1, which can -serve as a reference for developers. - -* allowing + signs in py.path.svn urls [39106] -* fixed support for Failed exceptions without excinfo in py.test [39340] -* added support for killing processes for Windows (as well as platforms that - support os.kill) in py.misc.killproc [39655] -* added setup/teardown for generative tests to py.test [40702] -* added detection of FAILED TO LOAD MODULE to py.test [40703, 40738, 40739] -* fixed problem with calling .remove() on wcpaths of non-versioned files in - py.path [44248] -* fixed some import and inheritance issues in py.test [41480, 44648, 44655] -* fail to run greenlet tests when pypy is available, but without stackless - [45294] -* small fixes in rsession tests [45295] -* fixed issue with 2.5 type representations in py.test [45483, 45484] -* made that internal reporting issues displaying is done atomically in py.test - [45518] -* made that non-existing files are igored by the py.lookup script [45519] -* improved exception name creation in py.test [45535] -* made that less threads are used in execnet [merge in 45539] -* removed lock required for atomical reporting issue displaying in py.test - [45545] -* removed globals from execnet [45541, 45547] -* refactored cleanup mechanics, made that setDaemon is set to 1 to make atexit - get called in 2.5 (py.execnet) [45548] -* fixed bug in joining threads in py.execnet's servemain [45549] -* refactored py.test.rsession tests to not rely on exact output format anymore - [45646] -* using repr() on test outcome [45647] -* added 'Reason' classes for py.test.skip() [45648, 45649] -* killed some unnecessary sanity check in py.test.collect [45655] -* avoid using os.tmpfile() in py.io.fdcapture because on Windows it's only - usable by Administrators [45901] -* added support for locking and non-recursive commits to py.path.svnwc [45994] -* locking files in py.execnet to prevent CPython from segfaulting [46010] -* added export() method to py.path.svnurl -* fixed -d -x in py.test [47277] -* fixed argument concatenation problem in py.path.svnwc [49423] -* restore py.test behaviour that it exits with code 1 when there are failures - [49974] -* don't fail on html files that don't have an accompanying .txt file [50606] -* fixed 'utestconvert.py < input' [50645] -* small fix for code indentation in py.code.source [50755] -* fix _docgen.py documentation building [51285] -* improved checks for source representation of code blocks in py.test [51292] -* added support for passing authentication to py.path.svn* objects [52000, - 52001] -* removed sorted() call for py.apigen tests in favour of [].sort() to support - Python 2.3 [52481] diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.3.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.3.txt deleted file mode 100644 index c62cb859053..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.3.txt +++ /dev/null @@ -1,26 +0,0 @@ -py.test/pylib 1.3.3: windows and other fixes -=========================================================================== - -pylib/py.test 1.3.3 is a minor bugfix release featuring some improvements -and fixes. See changelog_ for full history. - -have fun, -holger krekel - -.. _changelog: ../changelog.html - -Changes between 1.3.2 and 1.3.3 -================================================== - -- fix issue113: assertion representation problem with triple-quoted strings - (and possibly other cases) -- make conftest loading detect that a conftest file with the same - content was already loaded, avoids surprises in nested directory structures - which can be produced e.g. by Hudson. It probably removes the need to use - --confcutdir in most cases. -- fix terminal coloring for win32 - (thanks Michael Foord for reporting) -- fix weirdness: make terminal width detection work on stdout instead of stdin - (thanks Armin Ronacher for reporting) -- remove trailing whitespace in all py/text distribution files - diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.4.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.4.txt deleted file mode 100644 index c156c8bdb33..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.3.4.txt +++ /dev/null @@ -1,22 +0,0 @@ -py.test/pylib 1.3.4: fixes and new native traceback option -=========================================================================== - -pylib/py.test 1.3.4 is a minor maintenance release mostly containing bug fixes -and a new "--tb=native" traceback option to show "normal" Python standard -tracebacks instead of the py.test enhanced tracebacks. See below for more -change info and http://pytest.org for more general information on features -and configuration of the testing tool. - -Thanks to the issue reporters and generally to Ronny Pfannschmidt for help. - -cheers, -holger krekel - -Changes between 1.3.3 and 1.3.4 -================================================== - -- fix issue111: improve install documentation for windows -- fix issue119: fix custom collectability of __init__.py as a module -- fix issue116: --doctestmodules work with __init__.py files as well -- fix issue115: unify internal exception passthrough/catching/GeneratorExit -- fix issue118: new --tb=native for presenting cpython-standard exceptions diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.0.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.0.txt deleted file mode 100644 index 1c9fa75604a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.0.txt +++ /dev/null @@ -1,47 +0,0 @@ - -.. _`release-1.4.0`: - -py-1.4.0: cross-python lib for path, code, io, ... manipulations -=========================================================================== - -"py" is a small library comprising APIs for filesystem and svn path -manipulations, dynamic code construction and introspection, a Py2/Py3 -compatibility namespace ("py.builtin"), IO capturing, terminal colored printing -(on windows and linux), ini-file parsing and a lazy import mechanism. -It runs unmodified on all Python interpreters compatible to Python2.4 up -until Python 3.2. The general goal with "py" is to provide stable APIs -for some common tasks that are continously tested against many Python -interpreters and thus also to help transition. Here are some docs: - - http://pylib.org - -NOTE: The prior py-1.3.X versions contained "py.test" which now comes -as its own separate "pytest" distribution and was just released -as "pytest-2.0.0", see here for the revamped docs: - - http://pytest.org - -And "py.cleanup|py.lookup|py.countloc" etc. helpers are now part of -the pycmd distribution, see https://pypi.org/project/pycmd/ - -This makes "py-1.4.0" a simple library which does not install -any command line utilities anymore. - -cheers, -holger - -Changes between 1.3.4 and 1.4.0 -------------------------------------- - -- py.test was moved to a separate "pytest" package. What remains is - a stub hook which will proxy ``import py.test`` to ``pytest``. -- all command line tools ("py.cleanup/lookup/countloc/..." moved - to "pycmd" package) -- removed the old and deprecated "py.magic" namespace -- use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available -- add py.iniconfig module for brain-dead easy ini-config file parsing -- introduce py.builtin.any() -- path objects have a .dirname attribute now (equivalent to - os.path.dirname(path)) -- path.visit() accepts breadthfirst (bf) and sort options -- remove deprecated py.compat namespace diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.1.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.1.txt deleted file mode 100644 index 6ed72aa4183..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/release-1.4.1.txt +++ /dev/null @@ -1,47 +0,0 @@ - -.. _`release-1.4.1`: - -py-1.4.1: cross-python lib for fs path, code, io, ... manipulations -=========================================================================== - -This is a bug fix release of the "py" lib, see below for detailed changes. -The py lib is a small library comprising APIs for filesystem and svn path -manipulations, dynamic code construction and introspection, a Py2/Py3 -compatibility namespace ("py.builtin"), IO capturing, terminal colored printing -(on windows and linux), ini-file parsing and a lazy import mechanism. -It runs unmodified on all Python interpreters compatible to Python2.4 up -until Python 3.2, PyPy and Jython. The general goal with "py" is to -provide stable APIs for some common tasks that are continously tested -against many Python interpreters and thus also to help transition. Here -are some docs: - - http://pylib.org - -NOTE: The prior py-1.3.X versions contained "py.test" which since py-1.4.0 -comes as its own separate "pytest" distribution, see: - - http://pytest.org - -Also, the "py.cleanup|py.lookup|py.countloc" helpers are now part of -the pycmd distribution, see https://pypi.org/project/pycmd/ - - -Changes between 1.4.0 and 1.4.1 -================================================== - -- fix issue1 - py.error.* classes to be pickleable - -- fix issue2 - on windows32 use PATHEXT as the list of potential - extensions to find find binaries with py.path.local.sysfind(commandname) - -- fix (pytest-) issue10 and refine assertion reinterpretation - to avoid breaking if the __nonzero__ of an object fails - -- fix (pytest-) issue17 where python3 does not like star-imports, - leading to misrepresentation of import-errors in test modules - -- fix ``py.error.*`` attribute pypy access - -- allow path.samefile(arg) to succeed when arg is a relative filename - -- fix (pytest-) issue20 path.samefile(relpath) works as expected now diff --git a/tests/wpt/tests/tools/third_party/py/doc/announce/releases.txt b/tests/wpt/tests/tools/third_party/py/doc/announce/releases.txt deleted file mode 100644 index 309c29bac5d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/announce/releases.txt +++ /dev/null @@ -1,16 +0,0 @@ -============= -Release notes -============= - -Contents: - -.. toctree:: - :maxdepth: 2 - -.. include: release-1.1.0 -.. include: release-1.0.2 - - release-1.0.1 - release-1.0.0 - release-0.9.2 - release-0.9.0 diff --git a/tests/wpt/tests/tools/third_party/py/doc/changelog.txt b/tests/wpt/tests/tools/third_party/py/doc/changelog.txt deleted file mode 100644 index 0c9d0928e7a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/changelog.txt +++ /dev/null @@ -1,3 +0,0 @@ -.. _`changelog`: - -.. include:: ../CHANGELOG.rst diff --git a/tests/wpt/tests/tools/third_party/py/doc/code.txt b/tests/wpt/tests/tools/third_party/py/doc/code.txt deleted file mode 100644 index bdd8691da03..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/code.txt +++ /dev/null @@ -1,150 +0,0 @@ -================================================================================ -py.code: higher level python code and introspection objects -================================================================================ - -``py.code`` provides higher level APIs and objects for Code, Frame, Traceback, -ExceptionInfo and source code construction. The ``py.code`` library -tries to simplify accessing the code objects as well as creating them. -There is a small set of interfaces a user needs to deal with, all nicely -bundled together, and with a rich set of 'Pythonic' functionality. - -Contents of the library -======================= - -Every object in the ``py.code`` library wraps a code Python object related -to code objects, source code, frames and tracebacks: the ``py.code.Code`` -class wraps code objects, ``py.code.Source`` source snippets, -``py.code.Traceback` exception tracebacks, ``py.code.Frame`` frame -objects (as found in e.g. tracebacks) and ``py.code.ExceptionInfo`` the -tuple provided by sys.exc_info() (containing exception and traceback -information when an exception occurs). Also in the library is a helper function -``py.code.compile()`` that provides the same functionality as Python's -built-in 'compile()' function, but returns a wrapped code object. - -The wrappers -============ - -``py.code.Code`` -------------------- - -Code objects are instantiated with a code object or a callable as argument, -and provide functionality to compare themselves with other Code objects, get to -the source file or its contents, create new Code objects from scratch, etc. - -A quick example:: - - >>> import py - >>> c = py.code.Code(py.path.local.read) - >>> c.path.basename - 'common.py' - >>> isinstance(c.source(), py.code.Source) - True - >>> str(c.source()).split('\n')[0] - "def read(self, mode='r'):" - -.. autoclass:: py.code.Code - :members: - :inherited-members: - - -``py.code.Source`` ---------------------- - -Source objects wrap snippets of Python source code, providing a simple yet -powerful interface to read, deindent, slice, compare, compile and manipulate -them, things that are not so easy in core Python. - -Example:: - - >>> s = py.code.Source("""\ - ... def foo(): - ... print "foo" - ... """) - >>> str(s).startswith('def') # automatic de-indentation! - True - >>> s.isparseable() - True - >>> sub = s.getstatement(1) # get the statement starting at line 1 - >>> str(sub).strip() # XXX why is the strip() required?!? - 'print "foo"' - -.. autoclass:: py.code.Source - :members: - - -``py.code.Traceback`` ------------------------- - -Tracebacks are usually not very easy to examine, you need to access certain -somewhat hidden attributes of the traceback's items (resulting in expressions -such as 'fname = tb.tb_next.tb_frame.f_code.co_filename'). The Traceback -interface (and its TracebackItem children) tries to improve this. - -Example:: - - >>> import sys - >>> try: - ... py.path.local(100) # illegal argument - ... except: - ... exc, e, tb = sys.exc_info() - >>> t = py.code.Traceback(tb) - >>> first = t[1] # get the second entry (first is in this doc) - >>> first.path.basename # second is in py/path/local.py - 'local.py' - >>> isinstance(first.statement, py.code.Source) - True - >>> str(first.statement).strip().startswith('raise ValueError') - True - -.. autoclass:: py.code.Traceback - :members: - -``py.code.Frame`` --------------------- - -Frame wrappers are used in ``py.code.Traceback`` items, and will usually not -directly be instantiated. They provide some nice methods to evaluate code -'inside' the frame (using the frame's local variables), get to the underlying -code (frames have a code attribute that points to a ``py.code.Code`` object) -and examine the arguments. - -Example (using the 'first' TracebackItem instance created above):: - - >>> frame = first.frame - >>> isinstance(frame.code, py.code.Code) - True - >>> isinstance(frame.eval('self'), py.path.local) - True - >>> [namevalue[0] for namevalue in frame.getargs()] - ['cls', 'path'] - -.. autoclass:: py.code.Frame - :members: - -``py.code.ExceptionInfo`` ----------------------------- - -A wrapper around the tuple returned by sys.exc_info() (will call sys.exc_info() -itself if the tuple is not provided as an argument), provides some handy -attributes to easily access the traceback and exception string. - -Example:: - - >>> import sys - >>> try: - ... foobar() - ... except: - ... excinfo = py.code.ExceptionInfo() - >>> excinfo.typename - 'NameError' - >>> isinstance(excinfo.traceback, py.code.Traceback) - True - >>> excinfo.exconly() - "NameError: name 'foobar' is not defined" - -.. autoclass:: py.code.ExceptionInfo - :members: - -.. autoclass:: py.code.Traceback - :members: - diff --git a/tests/wpt/tests/tools/third_party/py/doc/conf.py b/tests/wpt/tests/tools/third_party/py/doc/conf.py deleted file mode 100644 index de4cbf8a46f..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/conf.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -# -# py documentation build configuration file, created by -# sphinx-quickstart on Thu Oct 21 08:30:10 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# 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.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# 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.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.txt' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'py' -copyright = u'2010, holger krekel et. al.' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -# The full version, including alpha/beta/rc tags. -import py -release = py.__version__ -version = ".".join(release.split(".")[:2]) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- 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 = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# "<project> v<release> documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# 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'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'py' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'py.tex', u'py Documentation', - u'holger krekel et. al.', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'py', u'py Documentation', - [u'holger krekel et. al.'], 1) -] - -autodoc_member_order = "bysource" -autodoc_default_flags = "inherited-members" - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'py' -epub_author = u'holger krekel et. al.' -epub_publisher = u'holger krekel et. al.' -epub_copyright = u'2010, holger krekel et. al.' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/tests/wpt/tests/tools/third_party/py/doc/download.html b/tests/wpt/tests/tools/third_party/py/doc/download.html deleted file mode 100644 index 5f4c466402d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/download.html +++ /dev/null @@ -1,18 +0,0 @@ -<html> - <head> - <meta http-equiv="refresh" content=" 1 ; URL=install.html" /> - </head> - - <body> -<script type="text/javascript"> -var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); -document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); -</script> -<script type="text/javascript"> -try { -var pageTracker = _gat._getTracker("UA-7597274-3"); -pageTracker._trackPageview(); -} catch(err) {}</script> -</body> -</html> - diff --git a/tests/wpt/tests/tools/third_party/py/doc/example/genhtml.py b/tests/wpt/tests/tools/third_party/py/doc/example/genhtml.py deleted file mode 100644 index 7a6d4934970..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/example/genhtml.py +++ /dev/null @@ -1,13 +0,0 @@ -from py.xml import html - -paras = "First Para", "Second para" - -doc = html.html( - html.head( - html.meta(name="Content-Type", value="text/html; charset=latin1")), - html.body( - [html.p(p) for p in paras])) - -print(unicode(doc).encode('latin1')) - - diff --git a/tests/wpt/tests/tools/third_party/py/doc/example/genhtmlcss.py b/tests/wpt/tests/tools/third_party/py/doc/example/genhtmlcss.py deleted file mode 100644 index facca77b78b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/example/genhtmlcss.py +++ /dev/null @@ -1,23 +0,0 @@ -import py -html = py.xml.html - -class my(html): - "a custom style" - class body(html.body): - style = html.Style(font_size = "120%") - - class h2(html.h2): - style = html.Style(background = "grey") - - class p(html.p): - style = html.Style(font_weight="bold") - -doc = my.html( - my.head(), - my.body( - my.h2("hello world"), - my.p("bold as bold can") - ) -) - -print(doc.unicode(indent=2)) diff --git a/tests/wpt/tests/tools/third_party/py/doc/example/genxml.py b/tests/wpt/tests/tools/third_party/py/doc/example/genxml.py deleted file mode 100644 index 444a4ca52cc..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/example/genxml.py +++ /dev/null @@ -1,17 +0,0 @@ - -import py -class ns(py.xml.Namespace): - pass - -doc = ns.books( - ns.book( - ns.author("May Day"), - ns.title("python for java programmers"),), - ns.book( - ns.author("why", class_="somecssclass"), - ns.title("Java for Python programmers"),), - publisher="N.N", - ) -print(doc.unicode(indent=2).encode('utf8')) - - diff --git a/tests/wpt/tests/tools/third_party/py/doc/faq.txt b/tests/wpt/tests/tools/third_party/py/doc/faq.txt deleted file mode 100644 index 6d374e1db9b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/faq.txt +++ /dev/null @@ -1,170 +0,0 @@ -================================== -Frequently Asked Questions -================================== - -.. contents:: - :local: - :depth: 2 - - -On naming, nosetests, licensing and magic -=========================================== - -Why the ``py`` naming? Why not ``pytest``? ----------------------------------------------------- - -This mostly has historic reasons - the aim is -to get away from the somewhat questionable 'py' name -at some point. These days (2010) the 'py' library -almost completely comprises APIs that are used -by the ``py.test`` tool. There also are some -other uses, e.g. of the ``py.path.local()`` and -other path implementations. So it requires some -work to factor them out and do the shift. - -Why the ``py.test`` naming? ------------------------------------- - -because of TAB-completion under Bash/Shells. If you hit -``py.<TAB>`` you'll get a list of available development -tools that all share the ``py.`` prefix. Another motivation -was to unify the package ("py.test") and tool filename. - -What's py.test's relation to ``nosetests``? ---------------------------------------------- - -py.test and nose_ share basic philosophy when it comes -to running Python tests. In fact, -with py.test-1.1.0 it is ever easier to run many test suites -that currently work with ``nosetests``. nose_ was created -as a clone of ``py.test`` when py.test was in the ``0.8`` release -cycle so some of the newer features_ introduced with py.test-1.0 -and py.test-1.1 have no counterpart in nose_. - -.. _nose: https://nose.readthedocs.io/ -.. _features: test/features.html -.. _apipkg: https://pypi.org/project/apipkg/ - - -What's this "magic" with py.test? ----------------------------------------- - -issues where people have used the term "magic" in the past: - -* `py/__init__.py`_ uses the apipkg_ mechanism for lazy-importing - and full control on what API you get when importing "import py". - -* when an ``assert`` statement fails, py.test re-interprets the expression - to show intermediate values if a test fails. If your expression - has side effects the intermediate values may not be the same, obfuscating - the initial error (this is also explained at the command line if it happens). - ``py.test --no-assert`` turns off assert re-intepretation. - Sidenote: it is good practise to avoid asserts with side effects. - - -.. _`py namespaces`: index.html -.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py - -Where does my ``py.test`` come/import from? ----------------------------------------------- - -You can issue:: - - py.test --version - -which tells you both version and import location of the tool. - - -function arguments, parametrized tests and setup -==================================================== - -.. _funcargs: test/funcargs.html - -Is using funcarg- versus xUnit-based setup a style question? ---------------------------------------------------------------- - -It depends. For simple applications or for people experienced -with nose_ or unittest-style test setup using `xUnit style setup`_ -make some sense. For larger test suites, parametrized testing -or setup of complex test resources using funcargs_ is recommended. -Moreover, funcargs are ideal for writing advanced test support -code (like e.g. the monkeypatch_, the tmpdir_ or capture_ funcargs) -because the support code can register setup/teardown functions -in a managed class/module/function scope. - -.. _monkeypatch: test/plugin/monkeypatch.html -.. _tmpdir: test/plugin/tmpdir.html -.. _capture: test/plugin/capture.html -.. _`xUnit style setup`: test/xunit_setup.html -.. _`pytest_nose`: test/plugin/nose.html - -.. _`why pytest_pyfuncarg__ methods?`: - -Why the ``pytest_funcarg__*`` name for funcarg factories? ---------------------------------------------------------------- - -When experimenting with funcargs an explicit registration mechanism -was considered. But lacking a good use case for this indirection and -flexibility we decided to go for `Convention over Configuration`_ and -allow to directly specify the factory. Besides removing the need -for an indirection it allows to "grep" for ``pytest_funcarg__MYARG`` -and will safely find all factory functions for the ``MYARG`` function -argument. It helps to alleviate the de-coupling of function -argument usage and creation. - -.. _`Convention over Configuration`: https://en.wikipedia.org/wiki/Convention_over_configuration - -Can I yield multiple values from a factory function? ------------------------------------------------------ - -There are two conceptual reasons why yielding from a factory function -is not possible: - -* Calling factories for obtaining test function arguments - is part of setting up and running a test. At that - point it is not possible to add new test calls to - the test collection anymore. - -* If multiple factories yielded values there would - be no natural place to determine the combination - policy - in real-world examples some combinations - often should not run. - -Use the `pytest_generate_tests`_ hook to solve both issues -and implement the `parametrization scheme of your choice`_. - -.. _`pytest_generate_tests`: test/funcargs.html#parametrizing-tests -.. _`parametrization scheme of your choice`: https://holgerkrekel.net/2009/05/13/parametrizing-python-tests-generalized/ - - -py.test interaction with other packages -=============================================== - -Issues with py.test, multiprocess and setuptools? ------------------------------------------------------------- - -On windows the multiprocess package will instantiate sub processes -by pickling and thus implicitely re-import a lot of local modules. -Unfortuantely, setuptools-0.6.11 does not ``if __name__=='__main__'`` -protect its generated command line script. This leads to infinite -recursion when running a test that instantiates Processes. -There are these workarounds: - -* `install Distribute`_ as a drop-in replacement for setuptools - and install py.test - -* `directly use a checkout`_ which avoids all setuptools/Distribute - installation - -If those options are not available to you, you may also manually -fix the script that is created by setuptools by inserting an -``if __name__ == '__main__'``. Or you can create a "pytest.py" -script with this content and invoke that with the python version:: - - import py - if __name__ == '__main__': - py.cmdline.pytest() - -.. _`directly use a checkout`: install.html#directly-use-a-checkout - -.. _`install distribute`: https://pypi.org/project/distribute/ diff --git a/tests/wpt/tests/tools/third_party/py/doc/img/pylib.png b/tests/wpt/tests/tools/third_party/py/doc/img/pylib.png Binary files differdeleted file mode 100644 index 2e10d438866..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/img/pylib.png +++ /dev/null diff --git a/tests/wpt/tests/tools/third_party/py/doc/index.txt b/tests/wpt/tests/tools/third_party/py/doc/index.txt deleted file mode 100644 index c700b17e987..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/index.txt +++ /dev/null @@ -1,39 +0,0 @@ -.. py documentation master file, created by - sphinx-quickstart on Thu Oct 21 08:30:10 2010. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to py's documentation! -================================= - -see :ref:`CHANGELOG <changelog>` for latest changes. - -.. _`pytest distribution`: http://pytest.org - -Contents: - -.. toctree:: - - install - path - code - io - log - xml - misc - - :maxdepth: 2 - -.. toctree:: - :hidden: - - announce/release-2.0.0 - changelog - announce/* - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`search` - diff --git a/tests/wpt/tests/tools/third_party/py/doc/install.txt b/tests/wpt/tests/tools/third_party/py/doc/install.txt deleted file mode 100644 index 93c79e3b2d8..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/install.txt +++ /dev/null @@ -1,91 +0,0 @@ - -.. _`py`: -.. _`index page`: https://pypi.org/project/py/ - -installation info in a nutshell -=================================================== - -**PyPI name**: py_ - -**Pythons**: CPython 2.7, 3.5, 3.6, 3.7, PyPy-5.4 - -**Operating systems**: Linux, Windows, OSX, Unix - -**Requirements**: setuptools_ or Distribute_ - -**Installers**: ``easy_install`` and ``pip`` - -**Code repository**: https://github.com/pytest-dev/py - -easy install or pip ``py`` ------------------------------ - -Both `Distribute`_ and setuptools_ provide the ``easy_install`` -installation tool with which you can type into a command line window:: - - easy_install -U py - -to install the latest release of the py lib. The ``-U`` switch -will trigger an upgrade if you already have an older version installed. - -.. note:: - - As of version 1.4 py does not contain py.test anymore - you - need to install the new `pytest`_ distribution. - -.. _pytest: http://pytest.org - -Working from version control or a tarball ------------------------------------------------ - -To follow development or start experiments, checkout the -complete code and documentation source:: - - git clone https://github.com/pytest-dev/py - -Development takes place on the 'master' branch. - -You can also go to the python package index and -download and unpack a TAR file:: - - https://pypi.org/project/py/ - -activating a checkout with setuptools --------------------------------------------- - -With a working `Distribute`_ or setuptools_ installation you can type:: - - python setup.py develop - -in order to work inline with the tools and the lib of your checkout. - -.. _`no-setuptools`: - -.. _`directly use a checkout`: - -.. _`setuptools`: https://pypi.org/project/setuptools/ - - -Mailing list and issue tracker --------------------------------------- - -- `py-dev developers list`_ and `commit mailing list`_. - -- ``#pytest`` `on irc.libera.chat <ircs://irc.libera.chat:6697/#pytest>`_ IRC - channel for random questions (using an IRC client, `via webchat - <https://web.libera.chat/#pytest>`_, or `via Matrix - <https://matrix.to/#/%23pytest:libera.chat>`_). - -- `issue tracker`_ use the issue tracker to report - bugs or request features. - -.. _`issue tracker`: https://github.com/pytest-dev/py/issues - -.. _codespeak: http://codespeak.net/ -.. _`py-dev`: -.. _`development mailing list`: -.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev -.. _`py-svn`: -.. _`commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn - -.. include:: links.inc diff --git a/tests/wpt/tests/tools/third_party/py/doc/io.txt b/tests/wpt/tests/tools/third_party/py/doc/io.txt deleted file mode 100644 index c11308a6d28..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/io.txt +++ /dev/null @@ -1,59 +0,0 @@ -======= -py.io -======= - - -The 'py' lib provides helper classes for capturing IO during -execution of a program. - -IO Capturing examples -=============================================== - -``py.io.StdCapture`` ---------------------------- - -Basic Example:: - - >>> import py - >>> capture = py.io.StdCapture() - >>> print "hello" - >>> out,err = capture.reset() - >>> out.strip() == "hello" - True - -For calling functions you may use a shortcut:: - - >>> import py - >>> def f(): print "hello" - >>> res, out, err = py.io.StdCapture.call(f) - >>> out.strip() == "hello" - True - -``py.io.StdCaptureFD`` ---------------------------- - -If you also want to capture writes to the stdout/stderr -filedescriptors you may invoke:: - - >>> import py, sys - >>> capture = py.io.StdCaptureFD(out=False, in_=False) - >>> sys.stderr.write("world") - >>> out,err = capture.reset() - >>> err - 'world' - -py.io object reference -============================ - -.. autoclass:: py.io.StdCaptureFD - :members: - :inherited-members: - -.. autoclass:: py.io.StdCapture - :members: - :inherited-members: - -.. autoclass:: py.io.TerminalWriter - :members: - :inherited-members: - diff --git a/tests/wpt/tests/tools/third_party/py/doc/links.inc b/tests/wpt/tests/tools/third_party/py/doc/links.inc deleted file mode 100644 index b61d01c696a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/links.inc +++ /dev/null @@ -1,15 +0,0 @@ - -.. _`skipping plugin`: plugin/skipping.html -.. _`funcargs mechanism`: funcargs.html -.. _`doctest.py`: https://docs.python.org/library/doctest.html -.. _`xUnit style setup`: xunit_setup.html -.. _`pytest_nose`: plugin/nose.html -.. _`reStructured Text`: http://docutils.sourceforge.net -.. _`Python debugger`: http://docs.python.org/lib/module-pdb.html -.. _nose: https://nose.readthedocs.io/ -.. _pytest: https://pypi.org/project/pytest/ -.. _`setuptools`: https://pypi.org/project/setuptools/ -.. _`distribute`: https://pypi.org/project/distribute/ -.. _`pip`: https://pypi.org/project/pip/ -.. _`virtualenv`: https://pypi.org/project/virtualenv/ -.. _hudson: http://hudson-ci.org/ diff --git a/tests/wpt/tests/tools/third_party/py/doc/log.txt b/tests/wpt/tests/tools/third_party/py/doc/log.txt deleted file mode 100644 index ca60fcac250..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/log.txt +++ /dev/null @@ -1,208 +0,0 @@ -.. role:: code(literal) -.. role:: file(literal) - -.. XXX figure out how the code literals should be dealt with in sphinx. There is probably something builtin. - -======================================== -py.log documentation and musings -======================================== - - -Foreword -======== - -This document is an attempt to briefly state the actual specification of the -:code:`py.log` module. It was written by Francois Pinard and also contains -some ideas for enhancing the py.log facilities. - -NOTE that :code:`py.log` is subject to refactorings, it may change with -the next release. - -This document is meant to trigger or facilitate discussions. It shamelessly -steals from the `Agile Testing`__ comments, and from other sources as well, -without really trying to sort them out. - -__ http://agiletesting.blogspot.com/2005/06/keyword-based-logging-with-py-library.html - - -Logging organisation -==================== - -The :code:`py.log` module aims a niche comparable to the one of the -`logging module`__ found within the standard Python distributions, yet -with much simpler paradigms for configuration and usage. - -__ http://www.python.org/doc/2.4.2/lib/module-logging.html - -Holger Krekel, the main :code:`py` library developer, introduced -the idea of keyword-based logging and the idea of logging *producers* and -*consumers*. A log producer is an object used by the application code -to send messages to various log consumers. When you create a log -producer, you define a set of keywords that are then used to both route -the logging messages to consumers, and to prefix those messages. - -In fact, each log producer has a few keywords associated with it for -identification purposes. These keywords form a tuple of strings, and -may be used to later retrieve a particular log producer. - -A log producer may (or may not) be associated with a log consumer, meant -to handle log messages in particular ways. The log consumers can be -``STDOUT``, ``STDERR``, log files, syslog, the Windows Event Log, user -defined functions, etc. (Yet, logging to syslog or to the Windows Event -Log is only future plans for now). A log producer has never more than -one consumer at a given time, but it is possible to dynamically switch -a producer to use another consumer. On the other hand, a single log -consumer may be associated with many producers. - -Note that creating and associating a producer and a consumer is done -automatically when not otherwise overriden, so using :code:`py` logging -is quite comfortable even in the smallest programs. More typically, -the application programmer will likely design a hierarchy of producers, -and will select keywords appropriately for marking the hierarchy tree. -If a node of the hierarchical tree of producers has to be divided in -sub-trees, all producers in the sub-trees share, as a common prefix, the -keywords of the node being divided. In other words, we go further down -in the hierarchy of producers merely by adding keywords. - -Using the py.log library -================================ - -To use the :code:`py.log` library, the user must import it into a Python -application, create at least one log producer and one log consumer, have -producers and consumers associated, and finally call the log producers -as needed, giving them log messages. - -Importing ---------- - -Once the :code:`py` library is installed on your system, a mere:: - - import py - -holds enough magic for lazily importing the various facilities of the -:code:`py` library when they are first needed. This is really how -:code:`py.log` is made available to the application. For example, after -the above ``import py``, one may directly write ``py.log.Producer(...)`` -and everything should work fine, the user does not have to worry about -specifically importing more modules. - -Creating a producer -------------------- - -There are three ways for creating a log producer instance: - - + As soon as ``py.log`` is first evaluated within an application - program, a default log producer is created, and made available under - the name ``py.log.default``. The keyword ``default`` is associated - with that producer. - - + The ``py.log.Producer()`` constructor may be explicitly called - for creating a new instance of a log producer. That constructor - accepts, as an argument, the keywords that should be associated with - that producer. Keywords may be given either as a tuple of keyword - strings, or as a single space-separated string of keywords. - - + Whenever an attribute is *taken* out of a log producer instance, - for the first time that attribute is taken, a new log producer is - created. The keywords associated with that new producer are those - of the initial producer instance, to which is appended the name of - the attribute being taken. - -The last point is especially useful, as it allows using log producers -without further declarations, merely creating them *on-the-fly*. - -Creating a consumer -------------------- - -There are many ways for creating or denoting a log consumer: - - + A default consumer exists within the ``py.log`` facilities, which - has the effect of writing log messages on the Python standard output - stream. That consumer is associated at the very top of the producer - hierarchy, and as such, is called whenever no other consumer is - found. - - + The notation ``py.log.STDOUT`` accesses a log consumer which writes - log messages on the Python standard output stream. - - + The notation ``py.log.STDERR`` accesses a log consumer which writes - log messages on the Python standard error stream. - - + The ``py.log.File()`` constructor accepts, as argument, either a file - already opened in write mode or any similar file-like object, and - creates a log consumer able to write log messages onto that file. - - + The ``py.log.Path()`` constructor accepts a file name for its first - argument, and creates a log consumer able to write log messages into - that file. The constructor call accepts a few keyword parameters: - - + ``append``, which is ``False`` by default, may be used for - opening the file in append mode instead of write mode. - - + ``delayed_create``, which is ``False`` by default, maybe be used - for opening the file at the latest possible time. Consequently, - the file will not be created if it did not exist, and no actual - log message gets written to it. - - + ``buffering``, which is 1 by default, is used when opening the - file. Buffering can be turned off by specifying a 0 value. The - buffer size may also be selected through this argument. - - + Any user defined function may be used for a log consumer. Such a - function should accept a single argument, which is the message to - write, and do whatever is deemed appropriate by the programmer. - When the need arises, this may be an especially useful and flexible - feature. - - + The special value ``None`` means no consumer at all. This acts just - like if there was a consumer which would silently discard all log - messages sent to it. - -Associating producers and consumers ------------------------------------ - -Each log producer may have at most one log consumer associated with -it. A log producer gets associated with a log consumer through a -``py.log.setconsumer()`` call. That function accepts two arguments, -the first identifying a producer (a tuple of keyword strings or a single -space-separated string of keywords), the second specifying the precise -consumer to use for that producer. Until this function is called for a -producer, that producer does not have any explicit consumer associated -with it. - -Now, the hierarchy of log producers establishes which consumer gets used -whenever a producer has no explicit consumer. When a log producer -has no consumer explicitly associated with it, it dynamically and -recursively inherits the consumer of its parent node, that is, that node -being a bit closer to the root of the hierarchy. In other words, the -rightmost keywords of that producer are dropped until another producer -is found which has an explicit consumer. A nice side-effect is that, -by explicitly associating a consumer with a producer, all consumer-less -producers which appear under that producer, in the hierarchy tree, -automatically *inherits* that consumer. - -Writing log messages --------------------- - -All log producer instances are also functions, and this is by calling -them that log messages are generated. Each call to a producer object -produces the text for one log entry, which in turn, is sent to the log -consumer for that producer. - -The log entry displays, after a prefix identifying the log producer -being used, all arguments given in the call, converted to strings and -space-separated. (This is meant by design to be fairly similar to what -the ``print`` statement does in Python). The prefix itself is made up -of a colon-separated list of keywords associated with the producer, the -whole being set within square brackets. - -Note that the consumer is responsible for adding the newline at the end -of the log entry. That final newline is not part of the text for the -log entry. - -.. Other details -.. ------------- -.. XXX: fill in details -.. + Should speak about pickle-ability of :code:`py.log`. -.. -.. + What is :code:`log.get` (in :file:`logger.py`)? diff --git a/tests/wpt/tests/tools/third_party/py/doc/misc.txt b/tests/wpt/tests/tools/third_party/py/doc/misc.txt deleted file mode 100644 index 4b453482757..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/misc.txt +++ /dev/null @@ -1,93 +0,0 @@ -==================================== -Miscellaneous features of the py lib -==================================== - -Mapping the standard python library into py -=========================================== - -The ``py.std`` object allows lazy access to -standard library modules. For example, to get to the print-exception -functionality of the standard library you can write:: - - py.std.traceback.print_exc() - -without having to do anything else than the usual ``import py`` -at the beginning. You can access any other top-level standard -library module this way. This means that you will only trigger -imports of modules that are actually needed. Note that no attempt -is made to import submodules. - -Support for interaction with system utilities/binaries -====================================================== - -Currently, the py lib offers two ways to interact with -system executables. ``py.process.cmdexec()`` invokes -the shell in order to execute a string. The other -one, ``py.path.local``'s 'sysexec()' method lets you -directly execute a binary. - -Both approaches will raise an exception in case of a return- -code other than 0 and otherwise return the stdout-output -of the child process. - -The shell based approach ------------------------- - -You can execute a command via your system shell -by doing something like:: - - out = py.process.cmdexec('ls -v') - -However, the ``cmdexec`` approach has a few shortcomings: - -- it relies on the underlying system shell -- it neccessitates shell-escaping for expressing arguments -- it does not easily allow to "fix" the binary you want to run. -- it only allows to execute executables from the local - filesystem - -.. _sysexec: - -local paths have ``sysexec`` ----------------------------- - -In order to synchronously execute an executable file you -can use ``sysexec``:: - - binsvn.sysexec('ls', 'http://codespeak.net/svn') - -where ``binsvn`` is a path that points to the ``svn`` commandline -binary. Note that this function does not offer any shell-escaping -so you have to pass in already separated arguments. - -finding an executable local path --------------------------------- - -Finding an executable is quite different on multiple platforms. -Currently, the ``PATH`` environment variable based search on -unix platforms is supported:: - - py.path.local.sysfind('svn') - -which returns the first path whose ``basename`` matches ``svn``. -In principle, `sysfind` deploys platform specific algorithms -to perform the search. On Windows, for example, it may look -at the registry (XXX). - -To make the story complete, we allow to pass in a second ``checker`` -argument that is called for each found executable. For example, if -you have multiple binaries available you may want to select the -right version:: - - def mysvn(p): - """ check that the given svn binary has version 1.1. """ - line = p.execute('--version'').readlines()[0] - if line.find('version 1.1'): - return p - binsvn = py.path.local.sysfind('svn', checker=mysvn) - - -Cross-Python Version compatibility helpers -============================================= - -The ``py.builtin`` namespace provides a number of helpers that help to write python code compatible across Python interpreters, mainly Python2 and Python3. Type ``help(py.builtin)`` on a Python prompt for the selection of builtins. diff --git a/tests/wpt/tests/tools/third_party/py/doc/path.txt b/tests/wpt/tests/tools/third_party/py/doc/path.txt deleted file mode 100644 index 8f506d49232..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/path.txt +++ /dev/null @@ -1,264 +0,0 @@ -======= -py.path -======= - - **Note**: The 'py' library is in "maintenance mode" and so is not - recommended for new projects. Please check out - `pathlib <https://docs.python.org/3/library/pathlib.html>`_ or - `pathlib2 <https://pypi.org/project/pathlib2/>`_ for path - operations. - -The 'py' lib provides a uniform high-level api to deal with filesystems -and filesystem-like interfaces: ``py.path``. It aims to offer a central -object to fs-like object trees (reading from and writing to files, adding -files/directories, examining the types and structure, etc.), and out-of-the-box -provides a number of implementations of this API. - -py.path.local - local file system path -=============================================== - -.. _`local`: - -basic interactive example -------------------------------------- - -The first and most obvious of the implementations is a wrapper around a local -filesystem. It's just a bit nicer in usage than the regular Python APIs, and -of course all the functionality is bundled together rather than spread over a -number of modules. - - -.. sourcecode:: pycon - - >>> import py - >>> temppath = py.path.local('py.path_documentation') - >>> foopath = temppath.join('foo') # get child 'foo' (lazily) - >>> foopath.check() # check if child 'foo' exists - False - >>> foopath.write('bar') # write some data to it - >>> foopath.check() - True - >>> foopath.read() - 'bar' - >>> foofile = foopath.open() # return a 'real' file object - >>> foofile.read(1) - 'b' - -reference documentation ---------------------------------- - -.. autoclass:: py._path.local.LocalPath - :members: - :inherited-members: - -``py.path.svnurl`` and ``py.path.svnwc`` -================================================== - -Two other ``py.path`` implementations that the py lib provides wrap the -popular `Subversion`_ revision control system: the first (called 'svnurl') -by interfacing with a remote server, the second by wrapping a local checkout. -Both allow you to access relatively advanced features such as metadata and -versioning, and both in a way more user-friendly manner than existing other -solutions. - -Some example usage of ``py.path.svnurl``: - -.. sourcecode:: pycon - - .. >>> import py - .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') - >>> url = py.path.svnurl('http://codespeak.net/svn/py') - >>> info = url.info() - >>> info.kind - 'dir' - >>> firstentry = url.log()[-1] - >>> import time - >>> time.strftime('%Y-%m-%d', time.gmtime(firstentry.date)) - '2004-10-02' - -Example usage of ``py.path.svnwc``: - -.. sourcecode:: pycon - - .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') - >>> temp = py.path.local('py.path_documentation') - >>> wc = py.path.svnwc(temp.join('svnwc')) - >>> wc.checkout('http://codespeak.net/svn/py/dist/py/path/local') - >>> wc.join('local.py').check() - True - -.. _`Subversion`: http://subversion.tigris.org/ - -svn path related API reference ------------------------------------------ - -.. autoclass:: py._path.svnwc.SvnWCCommandPath - :members: - :inherited-members: - -.. autoclass:: py._path.svnurl.SvnCommandPath - :members: - :inherited-members: - -.. autoclass:: py._path.svnwc.SvnAuth - :members: - :inherited-members: - -Common vs. specific API, Examples -======================================== - -All Path objects support a common set of operations, suitable -for many use cases and allowing to transparently switch the -path object within an application (e.g. from "local" to "svnwc"). -The common set includes functions such as `path.read()` to read all data -from a file, `path.write()` to write data, `path.listdir()` to get a list -of directory entries, `path.check()` to check if a node exists -and is of a particular type, `path.join()` to get -to a (grand)child, `path.visit()` to recursively walk through a node's -children, etc. Only things that are not common on 'normal' filesystems (yet), -such as handling metadata (e.g. the Subversion "properties") require -using specific APIs. - -A quick 'cookbook' of small examples that will be useful 'in real life', -which also presents parts of the 'common' API, and shows some non-common -methods: - -Searching `.txt` files --------------------------------- - -Search for a particular string inside all files with a .txt extension in a -specific directory. - -.. sourcecode:: pycon - - >>> dirpath = temppath.ensure('testdir', dir=True) - >>> dirpath.join('textfile1.txt').write('foo bar baz') - >>> dirpath.join('textfile2.txt').write('frob bar spam eggs') - >>> subdir = dirpath.ensure('subdir', dir=True) - >>> subdir.join('textfile1.txt').write('foo baz') - >>> subdir.join('textfile2.txt').write('spam eggs spam foo bar spam') - >>> results = [] - >>> for fpath in dirpath.visit('*.txt'): - ... if 'bar' in fpath.read(): - ... results.append(fpath.basename) - >>> results.sort() - >>> results - ['textfile1.txt', 'textfile2.txt', 'textfile2.txt'] - -Working with Paths ----------------------------- - -This example shows the ``py.path`` features to deal with -filesystem paths Note that the filesystem is never touched, -all operations are performed on a string level (so the paths -don't have to exist, either): - -.. sourcecode:: pycon - - >>> p1 = py.path.local('/foo/bar') - >>> p2 = p1.join('baz/qux') - >>> p2 == py.path.local('/foo/bar/baz/qux') - True - >>> sep = py.path.local.sep - >>> p2.relto(p1).replace(sep, '/') # os-specific path sep in the string - 'baz/qux' - >>> p2.bestrelpath(p1).replace(sep, '/') - '../..' - >>> p2.join(p2.bestrelpath(p1)) == p1 - True - >>> p3 = p1 / 'baz/qux' # the / operator allows joining, too - >>> p2 == p3 - True - >>> p4 = p1 + ".py" - >>> p4.basename == "bar.py" - True - >>> p4.ext == ".py" - True - >>> p4.purebasename == "bar" - True - -This should be possible on every implementation of ``py.path``, so -regardless of whether the implementation wraps a UNIX filesystem, a Windows -one, or a database or object tree, these functions should be available (each -with their own notion of path seperators and dealing with conversions, etc.). - -Checking path types -------------------------------- - -Now we will show a bit about the powerful 'check()' method on paths, which -allows you to check whether a file exists, what type it is, etc.: - -.. sourcecode:: pycon - - >>> file1 = temppath.join('file1') - >>> file1.check() # does it exist? - False - >>> file1 = file1.ensure(file=True) # 'touch' the file - >>> file1.check() - True - >>> file1.check(dir=True) # is it a dir? - False - >>> file1.check(file=True) # or a file? - True - >>> file1.check(ext='.txt') # check the extension - False - >>> textfile = temppath.ensure('text.txt', file=True) - >>> textfile.check(ext='.txt') - True - >>> file1.check(basename='file1') # we can use all the path's properties here - True - -Setting svn-properties --------------------------------- - -As an example of 'uncommon' methods, we'll show how to read and write -properties in an ``py.path.svnwc`` instance: - -.. sourcecode:: pycon - - .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') - >>> wc.propget('foo') - '' - >>> wc.propset('foo', 'bar') - >>> wc.propget('foo') - 'bar' - >>> len(wc.status().prop_modified) # our own props - 1 - >>> msg = wc.revert() # roll back our changes - >>> len(wc.status().prop_modified) - 0 - -SVN authentication ----------------------------- - -Some uncommon functionality can also be provided as extensions, such as SVN -authentication: - -.. sourcecode:: pycon - - .. >>> if not py.test.config.option.urlcheck: raise ValueError('skipchunk') - >>> auth = py.path.SvnAuth('anonymous', 'user', cache_auth=False, - ... interactive=False) - >>> wc.auth = auth - >>> wc.update() # this should work - >>> path = wc.ensure('thisshouldnotexist.txt') - >>> try: - ... path.commit('testing') - ... except py.process.cmdexec.Error, e: - ... pass - >>> 'authorization failed' in str(e) - True - -Known problems / limitations -=================================== - -* The SVN path objects require the "svn" command line, - there is currently no support for python bindings. - Parsing the svn output can lead to problems, particularly - regarding if you have a non-english "locales" setting. - -* While the path objects basically work on windows, - there is no attention yet on making unicode paths - work or deal with the famous "8.3" filename issues. - - diff --git a/tests/wpt/tests/tools/third_party/py/doc/style.css b/tests/wpt/tests/tools/third_party/py/doc/style.css deleted file mode 100644 index 95e3ef07b23..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/style.css +++ /dev/null @@ -1,1044 +0,0 @@ -body,body.editor,body.body { - font: 110% "Times New Roman", Arial, Verdana, Helvetica, serif; - background: White; - color: Black; -} - -a, a.reference { - text-decoration: none; -} -a[href]:hover { text-decoration: underline; } - -img { - border: none; - vertical-align: middle; -} - -p, div.text { - text-align: left; - line-height: 1.5em; - margin: 0.5em 0em 0em 0em; -} - - - -p a:active { - color: Red; - background-color: transparent; -} - -p img { - border: 0; - margin: 0; -} - -img.inlinephoto { - padding: 0; - padding-right: 1em; - padding-top: 0.7em; - float: left; -} - -hr { - clear: both; - height: 1px; - color: #8CACBB; - background-color: transparent; -} - - -ul { - line-height: 1.5em; - /*list-style-image: url("bullet.gif"); */ - margin-left: 1.5em; - padding:0; -} - -ol { - line-height: 1.5em; - margin-left: 1.5em; - padding:0; -} - -ul a, ol a { - text-decoration: underline; -} - -dl { -} - -dd { - line-height: 1.5em; - margin-bottom: 1em; -} - -blockquote { - font-family: Times, "Times New Roman", serif; - font-style: italic; - font-size: 120%; -} - -code { - color: Black; - /*background-color: #dee7ec;*/ - /*background-color: #cccccc;*/ -} - -pre { - padding: 1em; - border: 1px dotted #8cacbb; - color: Black; - /* - background-color: #dee7ec; - background-color: #cccccc; - background-color: #dee7ec; - */ - overflow: auto; -} - - -.netscape4 { - display: none; -} - -/* main page styles */ - -/*a[href]:hover { color: black; text-decoration: underline; } -a[href]:link { color: black; text-decoration: underline; } -a[href] { color: black; text-decoration: underline; } -*/ - -span.menu_selected { - color: black; - text-decoration: none; - padding-right: 0.3em; - background-color: #cccccc; -} - - -a.menu { - /*color: #3ba6ec; */ - font: 120% Verdana, Helvetica, Arial, sans-serif; - text-decoration: none; - padding-right: 0.3em; -} - -a.menu[href]:visited, a.menu[href]:link{ - /*color: #3ba6ec; */ - text-decoration: none; -} - -a.menu[href]:hover { - /*color: black;*/ -} - -div#pagetitle{ - /*border-spacing: 20px;*/ - font: 160% Verdana, Helvetica, Arial, sans-serif; - color: #3ba6ec; - vertical-align: middle; - left: 80 px; - padding-bottom: 0.3em; -} - -a.wikicurrent { - font: 100% Verdana, Helvetica, Arial, sans-serif; - color: #3ba6ec; - vertical-align: middle; -} - - -table.body { - border: 0; - /*padding: 0; - border-spacing: 0px; - border-collapse: separate; - */ -} - -td.page-header-left { - padding: 5px; - /*border-bottom: 1px solid #444444;*/ -} - -td.page-header-top { - padding: 0; - - /*border-bottom: 1px solid #444444;*/ -} - -td.sidebar { - padding: 1 0 0 1; -} - -td.sidebar p.classblock { - padding: 0 5 0 5; - margin: 1 1 1 1; - border: 1px solid #444444; - background-color: #eeeeee; -} - -td.sidebar p.userblock { - padding: 0 5 0 5; - margin: 1 1 1 1; - border: 1px solid #444444; - background-color: #eeeeff; -} - -td.content { - padding: 1 5 1 5; - vertical-align: top; - width: 100%; -} - -p.ok-message { - background-color: #22bb22; - padding: 5 5 5 5; - color: white; - font-weight: bold; -} -p.error-message { - background-color: #bb2222; - padding: 5 5 5 5; - color: white; - font-weight: bold; -} - -p:first-child { - margin: 0 ; - padding: 0; -} - -/* style for forms */ -table.form { - padding: 2; - border-spacing: 0px; - border-collapse: separate; -} - -table.form th { - color: #333388; - text-align: right; - vertical-align: top; - font-weight: normal; -} -table.form th.header { - font-weight: bold; - background-color: #eeeeff; - text-align: left; -} - -table.form th.required { - font-weight: bold; -} - -table.form td { - color: #333333; - empty-cells: show; - vertical-align: top; -} - -table.form td.optional { - font-weight: bold; - font-style: italic; -} - -table.form td.html { - color: #777777; -} - -/* style for lists */ -table.list { - border-spacing: 0px; - border-collapse: separate; - vertical-align: top; - padding-top: 0; - width: 100%; -} - -table.list th { - padding: 0 4 0 4; - color: #404070; - background-color: #eeeeff; - border-right: 1px solid #404070; - border-top: 1px solid #404070; - border-bottom: 1px solid #404070; - vertical-align: top; - empty-cells: show; -} -table.list th a[href]:hover { color: #404070 } -table.list th a[href]:link { color: #404070 } -table.list th a[href] { color: #404070 } -table.list th.group { - background-color: #f4f4ff; - text-align: center; - font-size: 120%; -} - -table.list td { - padding: 0 4 0 4; - border: 0 2 0 2; - border-right: 1px solid #404070; - color: #404070; - background-color: white; - vertical-align: top; - empty-cells: show; -} - -table.list tr.normal td { - background-color: white; - white-space: nowrap; -} - -table.list tr.alt td { - background-color: #efefef; - white-space: nowrap; -} - -table.list td:first-child { - border-left: 1px solid #404070; - border-right: 1px solid #404070; -} - -table.list th:first-child { - border-left: 1px solid #404070; - border-right: 1px solid #404070; -} - -table.list tr.navigation th { - text-align: right; -} -table.list tr.navigation th:first-child { - border-right: none; - text-align: left; -} - - -/* style for message displays */ -table.messages { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.messages th.header{ - padding-top: 10px; - border-bottom: 1px solid gray; - font-weight: bold; - background-color: white; - color: #707040; -} - -table.messages th { - font-weight: bold; - color: black; - text-align: left; - border-bottom: 1px solid #afafaf; -} - -table.messages td { - font-family: monospace; - background-color: #efefef; - border-bottom: 1px solid #afafaf; - color: black; - empty-cells: show; - border-right: 1px solid #afafaf; - vertical-align: top; - padding: 2 5 2 5; -} - -table.messages td:first-child { - border-left: 1px solid #afafaf; - border-right: 1px solid #afafaf; -} - -/* style for file displays */ -table.files { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.files th.header{ - padding-top: 10px; - border-bottom: 1px solid gray; - font-weight: bold; - background-color: white; - color: #707040; -} - -table.files th { - border-bottom: 1px solid #afafaf; - font-weight: bold; - text-align: left; -} - -table.files td { - font-family: monospace; - empty-cells: show; -} - -/* style for history displays */ -table.history { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.history th.header{ - padding-top: 10px; - border-bottom: 1px solid gray; - font-weight: bold; - background-color: white; - color: #707040; - font-size: 100%; -} - -table.history th { - border-bottom: 1px solid #afafaf; - font-weight: bold; - text-align: left; - font-size: 90%; -} - -table.history td { - font-size: 90%; - vertical-align: top; - empty-cells: show; -} - - -/* style for class list */ -table.classlist { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.classlist th.header{ - padding-top: 10px; - border-bottom: 1px solid gray; - font-weight: bold; - background-color: white; - color: #707040; -} - -table.classlist th { - font-weight: bold; - text-align: left; -} - - -/* style for class help display */ -table.classhelp { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.classhelp th { - font-weight: bold; - text-align: left; - color: #707040; -} - -table.classhelp td { - padding: 2 2 2 2; - border: 1px solid black; - text-align: left; - vertical-align: top; - empty-cells: show; -} - - -/* style for "other" displays */ -table.otherinfo { - border-spacing: 0px; - border-collapse: separate; - width: 100%; -} - -table.otherinfo th.header{ - padding-top: 10px; - border-bottom: 1px solid gray; - font-weight: bold; - background-color: white; - color: #707040; -} - -table.otherinfo th { - border-bottom: 1px solid #afafaf; - font-weight: bold; - text-align: left; -} - -input { - border: 1px solid #8cacbb; - color: Black; - background-color: white; - vertical-align: middle; - margin-bottom: 1px; /* IE bug fix */ - padding: 0.1em; -} - -select { - border: 1px solid #8cacbb; - color: Black; - background-color: white; - vertical-align: middle; - margin-bottom: 1px; /* IE bug fix */ - padding: 0.1em; -} - - -a.nonexistent { - color: #FF2222; -} -a.nonexistent:visited { - color: #FF2222; -} -a.external { - color: #AA6600; -} - -/* -dl,ul,ol { - margin-top: 1pt; -} -tt,pre { - font-family: Lucida Console,Courier New,Courier,monotype; - font-size: 12pt; -} -pre.code { - margin-top: 8pt; - margin-bottom: 8pt; - background-color: #FFFFEE; - white-space:pre; - border-style:solid; - border-width:1pt; - border-color:#999999; - color:#111111; - padding:5px; - width:100%; -} -*/ -div.diffold { - background-color: #FFFF80; - border-style:none; - border-width:thin; - width:100%; -} -div.diffnew { - background-color: #80FF80; - border-style:none; - border-width:thin; - width:100%; -} -div.message { - margin-top: 6pt; - background-color: #E8FFE8; - border-style:solid; - border-width:1pt; - border-color:#999999; - color:#440000; - padding:5px; - width:100%; -} -strong.highlight { - background-color: #FFBBBB; -/* as usual, NetScape breaks with innocent CSS - border-color: #FFAAAA; - border-style: solid; - border-width: 1pt; -*/ -} - -table.navibar { - background-color: #C8C8C8; - border-spacing: 3px; -} -td.navibar { - background-color: #E8E8E8; - vertical-align: top; - text-align: right; - padding: 0px; -} - -a#versioninfo { - color: blue; -} - -div#pagename { - font-size: 140%; - color: blue; - text-align: center; - font-weight: bold; - background-color: white; - padding: 0 ; -} - -a.wikiaction, input.wikiaction { - color: black; - text-decoration: None; - text-align: center; - color: black; - /*border: 1px solid #3ba6ec; */ - margin: 4px; - padding: 5; - padding-bottom: 0; - white-space: nowrap; -} - -a.wikiaction[href]:hover { - color: black; - text-decoration: none; - /*background-color: #dddddd; */ -} - - -div.legenditem { - padding-top: 0.5em; - padding-left: 0.3em; -} - -span.wikitoken { - background-color: #eeeeee; -} - - -div#contentspace h1:first-child, div.heading:first-child { - padding-top: 0; - margin-top: 0; -} -div#contentspace h2:first-child { - padding-top: 0; - margin-top: 0; -} - -/* heading and paragraph text */ - -div.heading, h1 { - font-family: Verdana, Helvetica, Arial, sans-serif; - background-color: #58b3ef; - background-color: #FFFFFF; - /*color: #4893cf;*/ - color: black; - padding-top: 1.0em; - padding-bottom:0.2em; - text-align: left; - margin-top: 0em; - /*margin-bottom:8pt;*/ - font-weight: bold; - font-size: 115%; - border-bottom: 1px solid #8CACBB; -} - -h2 { - border-bottom: 1px dotted #8CACBB; -} - - -h1, h2, h3, h4, h5, h6 { - color: Black; - clear: left; - font: 100% Verdana, Helvetica, Arial, sans-serif; - margin: 0; - padding-left: 0em; - padding-top: 1em; - padding-bottom: 0.2em; - /*border-bottom: 1px solid #8CACBB;*/ -} -/* h1,h2 { padding-top: 0; }*/ - - -h1 { font-size: 145%; } -h2 { font-size: 115%; } -h3 { font-size: 105%; } -h4 { font-size: 100%; } -h5 { font-size: 100%; } - -h1 a { text-decoration: None;} - -div.exception { - background-color: #bb2222; - padding: 5 5 5 5; - color: white; - font-weight: bold; -} -pre.exception { - font-size: 110%; - padding: 1em; - border: 1px solid #8cacbb; - color: Black; - background-color: #dee7ec; - background-color: #cccccc; -} - -/* defines for navgiation bar (documentation) */ - - -div.direntry { - padding-top: 0.3em; - padding-bottom: 0.3em; - margin-right: 1em; - font-weight: bold; - background-color: #dee7ec; - font-size: 110%; -} - -div.fileentry { - font-family: Verdana, Helvetica, Arial, sans-serif; - padding-bottom: 0.3em; - white-space: nowrap; - line-height: 150%; -} - -a.fileentry { - white-space: nowrap; -} - - -span.left { - text-align: left; -} -span.right { - text-align: right; -} - -div.navbar { - /*margin: 0;*/ - font-size: 80% /*smaller*/; - font-weight: bold; - text-align: left; - /* position: fixed; */ - top: 100pt; - left: 0pt; /* auto; */ - width: 120pt; - /* right: auto; - right: 0pt; 2em; */ -} - - -div.history a { - /* font-size: 70%; */ -} - -div.wikiactiontitle { - font-weight: bold; -} - -/* REST defines */ - -div.document { - margin: 0; -} - -h1.title { - margin: 0; - margin-bottom: 0.5em; -} - -td.toplist { - vertical-align: top; -} - -img#pyimg { - float: left; - padding-bottom: 1em; -} - -div#navspace { - position: absolute; - font-size: 100%; - width: 150px; - overflow: hidden; /* scroll; */ -} - - -div#errorline { - position: relative; - top: 5px; - float: right; -} - -div#contentspace { - position: absolute; - /* font: 120% "Times New Roman", serif;*/ - font: 110% Verdana, Helvetica, Arial, sans-serif; - left: 170px; - margin-right: 5px; -} - -div#menubar { -/* width: 400px; */ - float: left; -} - -/* for the documentation page */ -div#title{ - - font-size: 110%; - color: black; - - - /*background-color: #dee7ec; - #padding: 5pt; - #padding-bottom: 1em; - #color: black; - border-width: 1pt; - border-style: solid;*/ - -} - -div#docnavlist { - /*background-color: #dee7ec; */ - padding: 5pt; - padding-bottom: 2em; - color: black; - border-width: 1pt; - /*border-style: solid;*/ -} - - -/* text markup */ - -div.listtitle { - color: Black; - clear: left; - font: 120% Verdana, Helvetica, Arial, sans-serif; - margin: 0; - padding-left: 0em; - padding-top: 0em; - padding-bottom: 0.2em; - margin-right: 0.5em; - border-bottom: 1px solid #8CACBB; -} - -div.actionbox h3 { - padding-top: 0; - padding-right: 0.5em; - padding-left: 0.5em; - background-color: #fabf00; - text-align: center; - border: 1px solid black; /* 8cacbb; */ -} - -div.actionbox a { - display: block; - padding-bottom: 0.5em; - padding-top: 0.5em; - margin-left: 0.5em; -} - -div.actionbox a.history { - display: block; - padding-bottom: 0.5em; - padding-top: 0.5em; - margin-left: 0.5em; - font-size: 90%; -} - -div.actionbox { - margin-bottom: 2em; - padding-bottom: 1em; - overflow: hidden; /* scroll; */ -} - -/* taken from docutils (oh dear, a bit senseless) */ -ol.simple, ul.simple { - margin-bottom: 1em } - -ol.arabic { - list-style: decimal } - -ol.loweralpha { - list-style: lower-alpha } - -ol.upperalpha { - list-style: upper-alpha } - -ol.lowerroman { - list-style: lower-roman } - -ol.upperroman { - list-style: upper-roman } - - -/* -:Author: David Goodger -:Contact: goodger@users.sourceforge.net -:date: $Date: 2003/01/22 22:26:48 $ -:version: $Revision: 1.29 $ -:copyright: This stylesheet has been placed in the public domain. - -Default cascading style sheet for the HTML output of Docutils. -*/ -/* -.first { - margin-top: 0 } - -.last { - margin-bottom: 0 } - -a.toc-backref { - text-decoration: none ; - color: black } - -dd { - margin-bottom: 0.5em } - -div.abstract { - margin: 2em 5em } - -div.abstract p.topic-title { - font-weight: bold ; - text-align: center } - -div.attention, div.caution, div.danger, div.error, div.hint, -div.important, div.note, div.tip, div.warning { - margin: 2em ; - border: medium outset ; - padding: 1em } - -div.attention p.admonition-title, div.caution p.admonition-title, -div.danger p.admonition-title, div.error p.admonition-title, -div.warning p.admonition-title { - color: red ; - font-weight: bold ; - font-family: sans-serif } - -div.hint p.admonition-title, div.important p.admonition-title, -div.note p.admonition-title, div.tip p.admonition-title { - font-weight: bold ; - font-family: sans-serif } - -div.dedication { - margin: 2em 5em ; - text-align: center ; - font-style: italic } - -div.dedication p.topic-title { - font-weight: bold ; - font-style: normal } - -div.figure { - margin-left: 2em } - -div.footer, div.header { - font-size: smaller } - -div.system-messages { - margin: 5em } - -div.system-messages h1 { - color: red } - -div.system-message { - border: medium outset ; - padding: 1em } - -div.system-message p.system-message-title { - color: red ; - font-weight: bold } - -div.topic { - margin: 2em } - -h1.title { - text-align: center } - -h2.subtitle { - text-align: center } - -hr { - width: 75% } - -p.caption { - font-style: italic } - -p.credits { - font-style: italic ; - font-size: smaller } - -p.label { - white-space: nowrap } - -p.topic-title { - font-weight: bold } - -pre.address { - margin-bottom: 0 ; - margin-top: 0 ; - font-family: serif ; - font-size: 100% } - -pre.line-block { - font-family: serif ; - font-size: 100% } - -pre.literal-block, pre.doctest-block { - margin-left: 2em ; - margin-right: 2em ; - background-color: #eeeeee } - -span.classifier { - font-family: sans-serif ; - font-style: oblique } - -span.classifier-delimiter { - font-family: sans-serif ; - font-weight: bold } - -span.interpreted { - font-family: sans-serif } - -span.option { - white-space: nowrap } - -span.option-argument { - font-style: italic } - -span.pre { - white-space: pre } - -span.problematic { - color: red } - -table { - margin-top: 0.5em ; - margin-bottom: 0.5em } - -table.citation { - border-left: solid thin gray ; - padding-left: 0.5ex } - -table.docinfo { - margin: 2em 4em } - -table.footnote { - border-left: solid thin black ; - padding-left: 0.5ex } - -td, th { - padding-left: 0.5em ; - padding-right: 0.5em ; - vertical-align: top } - -th.docinfo-name, th.field-name { - font-weight: bold ; - text-align: left ; - white-space: nowrap } - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - font-size: 100% } - -tt { - background-color: #eeeeee } - -ul.auto-toc { - list-style-type: none } -*/ - -div.section { - margin-top: 1.0em ; -} diff --git a/tests/wpt/tests/tools/third_party/py/doc/xml.txt b/tests/wpt/tests/tools/third_party/py/doc/xml.txt deleted file mode 100644 index 1022de6e912..00000000000 --- a/tests/wpt/tests/tools/third_party/py/doc/xml.txt +++ /dev/null @@ -1,164 +0,0 @@ -==================================================== -py.xml: simple pythonic xml/html file generation -==================================================== - -Motivation -========== - -There are a plethora of frameworks and libraries to generate -xml and html trees. However, many of them are large, have a -steep learning curve and are often hard to debug. Not to -speak of the fact that they are frameworks to begin with. - -.. _xist: http://www.livinglogic.de/Python/xist/index.html - -a pythonic object model , please -================================ - -The py lib offers a pythonic way to generate xml/html, based on -ideas from xist_ which `uses python class objects`_ to build -xml trees. However, xist_'s implementation is somewhat heavy -because it has additional goals like transformations and -supporting many namespaces. But its basic idea is very easy. - -.. _`uses python class objects`: http://www.livinglogic.de/Python/xist/Howto.html - -generating arbitrary xml structures ------------------------------------ - -With ``py.xml.Namespace`` you have the basis -to generate custom xml-fragments on the fly:: - - class ns(py.xml.Namespace): - "my custom xml namespace" - doc = ns.books( - ns.book( - ns.author("May Day"), - ns.title("python for java programmers"),), - ns.book( - ns.author("why"), - ns.title("Java for Python programmers"),), - publisher="N.N", - ) - print doc.unicode(indent=2).encode('utf8') - -will give you this representation:: - - <books publisher="N.N"> - <book> - <author>May Day</author> - <title>python for java programmers</title></book> - <book> - <author>why</author> - <title>Java for Python programmers</title></book></books> - -In a sentence: positional arguments are child-tags and -keyword-arguments are attributes. - -On a side note, you'll see that the unicode-serializer -supports a nice indentation style which keeps your generated -html readable, basically through emulating python's white -space significance by putting closing-tags rightmost and -almost invisible at first glance :-) - -basic example for generating html ---------------------------------- - -Consider this example:: - - from py.xml import html # html namespace - - paras = "First Para", "Second para" - - doc = html.html( - html.head( - html.meta(name="Content-Type", value="text/html; charset=latin1")), - html.body( - [html.p(p) for p in paras])) - - print unicode(doc).encode('latin1') - -Again, tags are objects which contain tags and have attributes. -More exactly, Tags inherit from the list type and thus can be -manipulated as list objects. They additionally support a default -way to represent themselves as a serialized unicode object. - -If you happen to look at the py.xml implementation you'll -note that the tag/namespace implementation consumes some 50 lines -with another 50 lines for the unicode serialization code. - -CSS-styling your html Tags --------------------------- - -One aspect where many of the huge python xml/html generation -frameworks utterly fail is a clean and convenient integration -of CSS styling. Often, developers are left alone with keeping -CSS style definitions in sync with some style files -represented as strings (often in a separate .css file). Not -only is this hard to debug but the missing abstractions make -it hard to modify the styling of your tags or to choose custom -style representations (inline, html.head or external). Add the -Browers usual tolerance of messyness and errors in Style -references and welcome to hell, known as the domain of -developing web applications :-) - -By contrast, consider this CSS styling example:: - - class my(html): - "my initial custom style" - class body(html.body): - style = html.Style(font_size = "120%") - - class h2(html.h2): - style = html.Style(background = "grey") - - class p(html.p): - style = html.Style(font_weight="bold") - - doc = my.html( - my.head(), - my.body( - my.h2("hello world"), - my.p("bold as bold can") - ) - ) - - print doc.unicode(indent=2) - -This will give you a small'n mean self contained -represenation by default:: - - <html> - <head/> - <body style="font-size: 120%"> - <h2 style="background: grey">hello world</h2> - <p style="font-weight: bold">bold as bold can</p></body></html> - -Most importantly, note that the inline-styling is just an -implementation detail of the unicode serialization code. -You can easily modify the serialization to put your styling into the -``html.head`` or in a separate file and autogenerate CSS-class -names or ids. - -Hey, you could even write tests that you are using correct -styles suitable for specific browser requirements. Did i mention -that the ability to easily write tests for your generated -html and its serialization could help to develop _stable_ user -interfaces? - -More to come ... ----------------- - -For now, i don't think we should strive to offer much more -than the above. However, it is probably not hard to offer -*partial serialization* to allow generating maybe hundreds of -complex html documents per second. Basically we would allow -putting callables both as Tag content and as values of -attributes. A slightly more advanced Serialization would then -produce a list of unicode objects intermingled with callables. -At HTTP-Request time the callables would get called to -complete the probably request-specific serialization of -your Tags. Hum, it's probably harder to explain this than to -actually code it :-) - -.. _`py.test`: test/index.html diff --git a/tests/wpt/tests/tools/third_party/py/py/__init__.py b/tests/wpt/tests/tools/third_party/py/py/__init__.py deleted file mode 100644 index b892ce1a2a6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/__init__.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -pylib: rapid testing and development utils - -this module uses apipkg.py for lazy-loading sub modules -and classes. The initpkg-dictionary below specifies -name->value mappings where value can be another namespace -dictionary or an import path. - -(c) Holger Krekel and others, 2004-2014 -""" -from py._error import error - -try: - from py._vendored_packages import apipkg - lib_not_mangled_by_packagers = True - vendor_prefix = '._vendored_packages.' -except ImportError: - import apipkg - lib_not_mangled_by_packagers = False - vendor_prefix = '' - -try: - from ._version import version as __version__ -except ImportError: - # broken installation, we don't even try - __version__ = "unknown" - - -apipkg.initpkg(__name__, attr={'_apipkg': apipkg, 'error': error}, exportdefs={ - # access to all standard lib modules - 'std': '._std:std', - - '_pydir' : '.__metainfo:pydir', - 'version': 'py:__version__', # backward compatibility - - # pytest-2.0 has a flat namespace, we use alias modules - # to keep old references compatible - 'test' : 'pytest', - - # hook into the top-level standard library - 'process' : { - '__doc__' : '._process:__doc__', - 'cmdexec' : '._process.cmdexec:cmdexec', - 'kill' : '._process.killproc:kill', - 'ForkedFunc' : '._process.forkedfunc:ForkedFunc', - }, - - 'apipkg' : { - 'initpkg' : vendor_prefix + 'apipkg:initpkg', - 'ApiModule' : vendor_prefix + 'apipkg:ApiModule', - }, - - 'iniconfig' : { - 'IniConfig' : vendor_prefix + 'iniconfig:IniConfig', - 'ParseError' : vendor_prefix + 'iniconfig:ParseError', - }, - - 'path' : { - '__doc__' : '._path:__doc__', - 'svnwc' : '._path.svnwc:SvnWCCommandPath', - 'svnurl' : '._path.svnurl:SvnCommandPath', - 'local' : '._path.local:LocalPath', - 'SvnAuth' : '._path.svnwc:SvnAuth', - }, - - # python inspection/code-generation API - 'code' : { - '__doc__' : '._code:__doc__', - 'compile' : '._code.source:compile_', - 'Source' : '._code.source:Source', - 'Code' : '._code.code:Code', - 'Frame' : '._code.code:Frame', - 'ExceptionInfo' : '._code.code:ExceptionInfo', - 'Traceback' : '._code.code:Traceback', - 'getfslineno' : '._code.source:getfslineno', - 'getrawcode' : '._code.code:getrawcode', - 'patch_builtins' : '._code.code:patch_builtins', - 'unpatch_builtins' : '._code.code:unpatch_builtins', - '_AssertionError' : '._code.assertion:AssertionError', - '_reinterpret_old' : '._code.assertion:reinterpret_old', - '_reinterpret' : '._code.assertion:reinterpret', - '_reprcompare' : '._code.assertion:_reprcompare', - '_format_explanation' : '._code.assertion:_format_explanation', - }, - - # backports and additions of builtins - 'builtin' : { - '__doc__' : '._builtin:__doc__', - 'enumerate' : '._builtin:enumerate', - 'reversed' : '._builtin:reversed', - 'sorted' : '._builtin:sorted', - 'any' : '._builtin:any', - 'all' : '._builtin:all', - 'set' : '._builtin:set', - 'frozenset' : '._builtin:frozenset', - 'BaseException' : '._builtin:BaseException', - 'GeneratorExit' : '._builtin:GeneratorExit', - '_sysex' : '._builtin:_sysex', - 'print_' : '._builtin:print_', - '_reraise' : '._builtin:_reraise', - '_tryimport' : '._builtin:_tryimport', - 'exec_' : '._builtin:exec_', - '_basestring' : '._builtin:_basestring', - '_totext' : '._builtin:_totext', - '_isbytes' : '._builtin:_isbytes', - '_istext' : '._builtin:_istext', - '_getimself' : '._builtin:_getimself', - '_getfuncdict' : '._builtin:_getfuncdict', - '_getcode' : '._builtin:_getcode', - 'builtins' : '._builtin:builtins', - 'execfile' : '._builtin:execfile', - 'callable' : '._builtin:callable', - 'bytes' : '._builtin:bytes', - 'text' : '._builtin:text', - }, - - # input-output helping - 'io' : { - '__doc__' : '._io:__doc__', - 'dupfile' : '._io.capture:dupfile', - 'TextIO' : '._io.capture:TextIO', - 'BytesIO' : '._io.capture:BytesIO', - 'FDCapture' : '._io.capture:FDCapture', - 'StdCapture' : '._io.capture:StdCapture', - 'StdCaptureFD' : '._io.capture:StdCaptureFD', - 'TerminalWriter' : '._io.terminalwriter:TerminalWriter', - 'ansi_print' : '._io.terminalwriter:ansi_print', - 'get_terminal_width' : '._io.terminalwriter:get_terminal_width', - 'saferepr' : '._io.saferepr:saferepr', - }, - - # small and mean xml/html generation - 'xml' : { - '__doc__' : '._xmlgen:__doc__', - 'html' : '._xmlgen:html', - 'Tag' : '._xmlgen:Tag', - 'raw' : '._xmlgen:raw', - 'Namespace' : '._xmlgen:Namespace', - 'escape' : '._xmlgen:escape', - }, - - 'log' : { - # logging API ('producers' and 'consumers' connected via keywords) - '__doc__' : '._log:__doc__', - '_apiwarn' : '._log.warning:_apiwarn', - 'Producer' : '._log.log:Producer', - 'setconsumer' : '._log.log:setconsumer', - '_setstate' : '._log.log:setstate', - '_getstate' : '._log.log:getstate', - 'Path' : '._log.log:Path', - 'STDOUT' : '._log.log:STDOUT', - 'STDERR' : '._log.log:STDERR', - 'Syslog' : '._log.log:Syslog', - }, - -}) diff --git a/tests/wpt/tests/tools/third_party/py/py/__init__.pyi b/tests/wpt/tests/tools/third_party/py/py/__init__.pyi deleted file mode 100644 index 96859e310f4..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/__init__.pyi +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any - -# py allows to use e.g. py.path.local even without importing py.path. -# So import implicitly. -from . import error -from . import iniconfig -from . import path -from . import io -from . import xml - -__version__: str - -# Untyped modules below here. -std: Any -test: Any -process: Any -apipkg: Any -code: Any -builtin: Any -log: Any diff --git a/tests/wpt/tests/tools/third_party/py/py/__metainfo.py b/tests/wpt/tests/tools/third_party/py/py/__metainfo.py deleted file mode 100644 index 12581eb7afb..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/__metainfo.py +++ /dev/null @@ -1,2 +0,0 @@ -import py -pydir = py.path.local(py.__file__).dirpath() diff --git a/tests/wpt/tests/tools/third_party/py/py/_builtin.py b/tests/wpt/tests/tools/third_party/py/py/_builtin.py deleted file mode 100644 index ddc89fc7be6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_builtin.py +++ /dev/null @@ -1,149 +0,0 @@ -import sys - - -# Passthrough for builtins supported with py27. -BaseException = BaseException -GeneratorExit = GeneratorExit -_sysex = (KeyboardInterrupt, SystemExit, MemoryError, GeneratorExit) -all = all -any = any -callable = callable -enumerate = enumerate -reversed = reversed -set, frozenset = set, frozenset -sorted = sorted - - -if sys.version_info >= (3, 0): - exec("print_ = print ; exec_=exec") - import builtins - - # some backward compatibility helpers - _basestring = str - def _totext(obj, encoding=None, errors=None): - if isinstance(obj, bytes): - if errors is None: - obj = obj.decode(encoding) - else: - obj = obj.decode(encoding, errors) - elif not isinstance(obj, str): - obj = str(obj) - return obj - - def _isbytes(x): - return isinstance(x, bytes) - - def _istext(x): - return isinstance(x, str) - - text = str - bytes = bytes - - def _getimself(function): - return getattr(function, '__self__', None) - - def _getfuncdict(function): - return getattr(function, "__dict__", None) - - def _getcode(function): - return getattr(function, "__code__", None) - - def execfile(fn, globs=None, locs=None): - if globs is None: - back = sys._getframe(1) - globs = back.f_globals - locs = back.f_locals - del back - elif locs is None: - locs = globs - fp = open(fn, "r") - try: - source = fp.read() - finally: - fp.close() - co = compile(source, fn, "exec", dont_inherit=True) - exec_(co, globs, locs) - -else: - import __builtin__ as builtins - _totext = unicode - _basestring = basestring - text = unicode - bytes = str - execfile = execfile - callable = callable - def _isbytes(x): - return isinstance(x, str) - def _istext(x): - return isinstance(x, unicode) - - def _getimself(function): - return getattr(function, 'im_self', None) - - def _getfuncdict(function): - return getattr(function, "__dict__", None) - - def _getcode(function): - try: - return getattr(function, "__code__") - except AttributeError: - return getattr(function, "func_code", None) - - def print_(*args, **kwargs): - """ minimal backport of py3k print statement. """ - sep = ' ' - if 'sep' in kwargs: - sep = kwargs.pop('sep') - end = '\n' - if 'end' in kwargs: - end = kwargs.pop('end') - file = 'file' in kwargs and kwargs.pop('file') or sys.stdout - if kwargs: - args = ", ".join([str(x) for x in kwargs]) - raise TypeError("invalid keyword arguments: %s" % args) - at_start = True - for x in args: - if not at_start: - file.write(sep) - file.write(str(x)) - at_start = False - file.write(end) - - def exec_(obj, globals=None, locals=None): - """ minimal backport of py3k exec statement. """ - __tracebackhide__ = True - if globals is None: - frame = sys._getframe(1) - globals = frame.f_globals - if locals is None: - locals = frame.f_locals - elif locals is None: - locals = globals - exec2(obj, globals, locals) - -if sys.version_info >= (3, 0): - def _reraise(cls, val, tb): - __tracebackhide__ = True - assert hasattr(val, '__traceback__') - raise cls.with_traceback(val, tb) -else: - exec (""" -def _reraise(cls, val, tb): - __tracebackhide__ = True - raise cls, val, tb -def exec2(obj, globals, locals): - __tracebackhide__ = True - exec obj in globals, locals -""") - -def _tryimport(*names): - """ return the first successfully imported module. """ - assert names - for name in names: - try: - __import__(name) - except ImportError: - excinfo = sys.exc_info() - else: - return sys.modules[name] - _reraise(*excinfo) diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_code/__init__.py deleted file mode 100644 index f15acf85132..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" python inspection/code generation API """ diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/_assertionnew.py b/tests/wpt/tests/tools/third_party/py/py/_code/_assertionnew.py deleted file mode 100644 index d03f29d8708..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/_assertionnew.py +++ /dev/null @@ -1,322 +0,0 @@ -""" -Find intermediate evalutation results in assert statements through builtin AST. -This should replace _assertionold.py eventually. -""" - -import sys -import ast - -import py -from py._code.assertion import _format_explanation, BuiltinAssertionError - - -def _is_ast_expr(node): - return isinstance(node, ast.expr) -def _is_ast_stmt(node): - return isinstance(node, ast.stmt) - - -class Failure(Exception): - """Error found while interpreting AST.""" - - def __init__(self, explanation=""): - self.cause = sys.exc_info() - self.explanation = explanation - - -def interpret(source, frame, should_fail=False): - mod = ast.parse(source) - visitor = DebugInterpreter(frame) - try: - visitor.visit(mod) - except Failure: - failure = sys.exc_info()[1] - return getfailure(failure) - if should_fail: - return ("(assertion failed, but when it was re-run for " - "printing intermediate values, it did not fail. Suggestions: " - "compute assert expression before the assert or use --no-assert)") - -def run(offending_line, frame=None): - if frame is None: - frame = py.code.Frame(sys._getframe(1)) - return interpret(offending_line, frame) - -def getfailure(failure): - explanation = _format_explanation(failure.explanation) - value = failure.cause[1] - if str(value): - lines = explanation.splitlines() - if not lines: - lines.append("") - lines[0] += " << %s" % (value,) - explanation = "\n".join(lines) - text = "%s: %s" % (failure.cause[0].__name__, explanation) - if text.startswith("AssertionError: assert "): - text = text[16:] - return text - - -operator_map = { - ast.BitOr : "|", - ast.BitXor : "^", - ast.BitAnd : "&", - ast.LShift : "<<", - ast.RShift : ">>", - ast.Add : "+", - ast.Sub : "-", - ast.Mult : "*", - ast.Div : "/", - ast.FloorDiv : "//", - ast.Mod : "%", - ast.Eq : "==", - ast.NotEq : "!=", - ast.Lt : "<", - ast.LtE : "<=", - ast.Gt : ">", - ast.GtE : ">=", - ast.Pow : "**", - ast.Is : "is", - ast.IsNot : "is not", - ast.In : "in", - ast.NotIn : "not in" -} - -unary_map = { - ast.Not : "not %s", - ast.Invert : "~%s", - ast.USub : "-%s", - ast.UAdd : "+%s" -} - - -class DebugInterpreter(ast.NodeVisitor): - """Interpret AST nodes to gleam useful debugging information. """ - - def __init__(self, frame): - self.frame = frame - - def generic_visit(self, node): - # Fallback when we don't have a special implementation. - if _is_ast_expr(node): - mod = ast.Expression(node) - co = self._compile(mod) - try: - result = self.frame.eval(co) - except Exception: - raise Failure() - explanation = self.frame.repr(result) - return explanation, result - elif _is_ast_stmt(node): - mod = ast.Module([node]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co) - except Exception: - raise Failure() - return None, None - else: - raise AssertionError("can't handle %s" %(node,)) - - def _compile(self, source, mode="eval"): - return compile(source, "<assertion interpretation>", mode) - - def visit_Expr(self, expr): - return self.visit(expr.value) - - def visit_Module(self, mod): - for stmt in mod.body: - self.visit(stmt) - - def visit_Name(self, name): - explanation, result = self.generic_visit(name) - # See if the name is local. - source = "%r in locals() is not globals()" % (name.id,) - co = self._compile(source) - try: - local = self.frame.eval(co) - except Exception: - # have to assume it isn't - local = False - if not local: - return name.id, result - return explanation, result - - def visit_Compare(self, comp): - left = comp.left - left_explanation, left_result = self.visit(left) - for op, next_op in zip(comp.ops, comp.comparators): - next_explanation, next_result = self.visit(next_op) - op_symbol = operator_map[op.__class__] - explanation = "%s %s %s" % (left_explanation, op_symbol, - next_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=next_result) - except Exception: - raise Failure(explanation) - try: - if not result: - break - except KeyboardInterrupt: - raise - except: - break - left_explanation, left_result = next_explanation, next_result - - rcomp = py.code._reprcompare - if rcomp: - res = rcomp(op_symbol, left_result, next_result) - if res: - explanation = res - return explanation, result - - def visit_BoolOp(self, boolop): - is_or = isinstance(boolop.op, ast.Or) - explanations = [] - for operand in boolop.values: - explanation, result = self.visit(operand) - explanations.append(explanation) - if result == is_or: - break - name = is_or and " or " or " and " - explanation = "(" + name.join(explanations) + ")" - return explanation, result - - def visit_UnaryOp(self, unary): - pattern = unary_map[unary.op.__class__] - operand_explanation, operand_result = self.visit(unary.operand) - explanation = pattern % (operand_explanation,) - co = self._compile(pattern % ("__exprinfo_expr",)) - try: - result = self.frame.eval(co, __exprinfo_expr=operand_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_BinOp(self, binop): - left_explanation, left_result = self.visit(binop.left) - right_explanation, right_result = self.visit(binop.right) - symbol = operator_map[binop.op.__class__] - explanation = "(%s %s %s)" % (left_explanation, symbol, - right_explanation) - source = "__exprinfo_left %s __exprinfo_right" % (symbol,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_left=left_result, - __exprinfo_right=right_result) - except Exception: - raise Failure(explanation) - return explanation, result - - def visit_Call(self, call): - func_explanation, func = self.visit(call.func) - arg_explanations = [] - ns = {"__exprinfo_func" : func} - arguments = [] - for arg in call.args: - arg_explanation, arg_result = self.visit(arg) - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - arguments.append(arg_name) - arg_explanations.append(arg_explanation) - for keyword in call.keywords: - arg_explanation, arg_result = self.visit(keyword.value) - arg_name = "__exprinfo_%s" % (len(ns),) - ns[arg_name] = arg_result - keyword_source = "%s=%%s" % (keyword.arg) - arguments.append(keyword_source % (arg_name,)) - arg_explanations.append(keyword_source % (arg_explanation,)) - if call.starargs: - arg_explanation, arg_result = self.visit(call.starargs) - arg_name = "__exprinfo_star" - ns[arg_name] = arg_result - arguments.append("*%s" % (arg_name,)) - arg_explanations.append("*%s" % (arg_explanation,)) - if call.kwargs: - arg_explanation, arg_result = self.visit(call.kwargs) - arg_name = "__exprinfo_kwds" - ns[arg_name] = arg_result - arguments.append("**%s" % (arg_name,)) - arg_explanations.append("**%s" % (arg_explanation,)) - args_explained = ", ".join(arg_explanations) - explanation = "%s(%s)" % (func_explanation, args_explained) - args = ", ".join(arguments) - source = "__exprinfo_func(%s)" % (args,) - co = self._compile(source) - try: - result = self.frame.eval(co, **ns) - except Exception: - raise Failure(explanation) - pattern = "%s\n{%s = %s\n}" - rep = self.frame.repr(result) - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def _is_builtin_name(self, name): - pattern = "%r not in globals() and %r not in locals()" - source = pattern % (name.id, name.id) - co = self._compile(source) - try: - return self.frame.eval(co) - except Exception: - return False - - def visit_Attribute(self, attr): - if not isinstance(attr.ctx, ast.Load): - return self.generic_visit(attr) - source_explanation, source_result = self.visit(attr.value) - explanation = "%s.%s" % (source_explanation, attr.attr) - source = "__exprinfo_expr.%s" % (attr.attr,) - co = self._compile(source) - try: - result = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - raise Failure(explanation) - explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result), - self.frame.repr(result), - source_explanation, attr.attr) - # Check if the attr is from an instance. - source = "%r in getattr(__exprinfo_expr, '__dict__', {})" - source = source % (attr.attr,) - co = self._compile(source) - try: - from_instance = self.frame.eval(co, __exprinfo_expr=source_result) - except Exception: - from_instance = True - if from_instance: - rep = self.frame.repr(result) - pattern = "%s\n{%s = %s\n}" - explanation = pattern % (rep, rep, explanation) - return explanation, result - - def visit_Assert(self, assrt): - test_explanation, test_result = self.visit(assrt.test) - if test_explanation.startswith("False\n{False =") and \ - test_explanation.endswith("\n"): - test_explanation = test_explanation[15:-2] - explanation = "assert %s" % (test_explanation,) - if not test_result: - try: - raise BuiltinAssertionError - except Exception: - raise Failure(explanation) - return explanation, test_result - - def visit_Assign(self, assign): - value_explanation, value_result = self.visit(assign.value) - explanation = "... = %s" % (value_explanation,) - name = ast.Name("__exprinfo_expr", ast.Load(), - lineno=assign.value.lineno, - col_offset=assign.value.col_offset) - new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno, - col_offset=assign.col_offset) - mod = ast.Module([new_assign]) - co = self._compile(mod, "exec") - try: - self.frame.exec_(co, __exprinfo_expr=value_result) - except Exception: - raise Failure(explanation) - return explanation, value_result diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/_assertionold.py b/tests/wpt/tests/tools/third_party/py/py/_code/_assertionold.py deleted file mode 100644 index 1bb70a875d0..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/_assertionold.py +++ /dev/null @@ -1,556 +0,0 @@ -import py -import sys, inspect -from compiler import parse, ast, pycodegen -from py._code.assertion import BuiltinAssertionError, _format_explanation -import types - -passthroughex = py.builtin._sysex - -class Failure: - def __init__(self, node): - self.exc, self.value, self.tb = sys.exc_info() - self.node = node - -class View(object): - """View base class. - - If C is a subclass of View, then C(x) creates a proxy object around - the object x. The actual class of the proxy is not C in general, - but a *subclass* of C determined by the rules below. To avoid confusion - we call view class the class of the proxy (a subclass of C, so of View) - and object class the class of x. - - Attributes and methods not found in the proxy are automatically read on x. - Other operations like setting attributes are performed on the proxy, as - determined by its view class. The object x is available from the proxy - as its __obj__ attribute. - - The view class selection is determined by the __view__ tuples and the - optional __viewkey__ method. By default, the selected view class is the - most specific subclass of C whose __view__ mentions the class of x. - If no such subclass is found, the search proceeds with the parent - object classes. For example, C(True) will first look for a subclass - of C with __view__ = (..., bool, ...) and only if it doesn't find any - look for one with __view__ = (..., int, ...), and then ..., object,... - If everything fails the class C itself is considered to be the default. - - Alternatively, the view class selection can be driven by another aspect - of the object x, instead of the class of x, by overriding __viewkey__. - See last example at the end of this module. - """ - - _viewcache = {} - __view__ = () - - def __new__(rootclass, obj, *args, **kwds): - self = object.__new__(rootclass) - self.__obj__ = obj - self.__rootclass__ = rootclass - key = self.__viewkey__() - try: - self.__class__ = self._viewcache[key] - except KeyError: - self.__class__ = self._selectsubclass(key) - return self - - def __getattr__(self, attr): - # attributes not found in the normal hierarchy rooted on View - # are looked up in the object's real class - return getattr(self.__obj__, attr) - - def __viewkey__(self): - return self.__obj__.__class__ - - def __matchkey__(self, key, subclasses): - if inspect.isclass(key): - keys = inspect.getmro(key) - else: - keys = [key] - for key in keys: - result = [C for C in subclasses if key in C.__view__] - if result: - return result - return [] - - def _selectsubclass(self, key): - subclasses = list(enumsubclasses(self.__rootclass__)) - for C in subclasses: - if not isinstance(C.__view__, tuple): - C.__view__ = (C.__view__,) - choices = self.__matchkey__(key, subclasses) - if not choices: - return self.__rootclass__ - elif len(choices) == 1: - return choices[0] - else: - # combine the multiple choices - return type('?', tuple(choices), {}) - - def __repr__(self): - return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) - - -def enumsubclasses(cls): - for subcls in cls.__subclasses__(): - for subsubclass in enumsubclasses(subcls): - yield subsubclass - yield cls - - -class Interpretable(View): - """A parse tree node with a few extra methods.""" - explanation = None - - def is_builtin(self, frame): - return False - - def eval(self, frame): - # fall-back for unknown expression nodes - try: - expr = ast.Expression(self.__obj__) - expr.filename = '<eval>' - self.__obj__.filename = '<eval>' - co = pycodegen.ExpressionCodeGenerator(expr).getCode() - result = frame.eval(co) - except passthroughex: - raise - except: - raise Failure(self) - self.result = result - self.explanation = self.explanation or frame.repr(self.result) - - def run(self, frame): - # fall-back for unknown statement nodes - try: - expr = ast.Module(None, ast.Stmt([self.__obj__])) - expr.filename = '<run>' - co = pycodegen.ModuleCodeGenerator(expr).getCode() - frame.exec_(co) - except passthroughex: - raise - except: - raise Failure(self) - - def nice_explanation(self): - return _format_explanation(self.explanation) - - -class Name(Interpretable): - __view__ = ast.Name - - def is_local(self, frame): - source = '%r in locals() is not globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_global(self, frame): - source = '%r in globals()' % self.name - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def is_builtin(self, frame): - source = '%r not in locals() and %r not in globals()' % ( - self.name, self.name) - try: - return frame.is_true(frame.eval(source)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - super(Name, self).eval(frame) - if not self.is_local(frame): - self.explanation = self.name - -class Compare(Interpretable): - __view__ = ast.Compare - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - for operation, expr2 in self.ops: - if hasattr(self, 'result'): - # shortcutting in chained expressions - if not frame.is_true(self.result): - break - expr2 = Interpretable(expr2) - expr2.eval(frame) - self.explanation = "%s %s %s" % ( - expr.explanation, operation, expr2.explanation) - source = "__exprinfo_left %s __exprinfo_right" % operation - try: - self.result = frame.eval(source, - __exprinfo_left=expr.result, - __exprinfo_right=expr2.result) - except passthroughex: - raise - except: - raise Failure(self) - expr = expr2 - -class And(Interpretable): - __view__ = ast.And - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if not frame.is_true(expr.result): - break - self.explanation = '(' + ' and '.join(explanations) + ')' - -class Or(Interpretable): - __view__ = ast.Or - - def eval(self, frame): - explanations = [] - for expr in self.nodes: - expr = Interpretable(expr) - expr.eval(frame) - explanations.append(expr.explanation) - self.result = expr.result - if frame.is_true(expr.result): - break - self.explanation = '(' + ' or '.join(explanations) + ')' - - -# == Unary operations == -keepalive = [] -for astclass, astpattern in { - ast.Not : 'not __exprinfo_expr', - ast.Invert : '(~__exprinfo_expr)', - }.items(): - - class UnaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - expr = Interpretable(self.expr) - expr.eval(frame) - self.explanation = astpattern.replace('__exprinfo_expr', - expr.explanation) - try: - self.result = frame.eval(astpattern, - __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(UnaryArith) - -# == Binary operations == -for astclass, astpattern in { - ast.Add : '(__exprinfo_left + __exprinfo_right)', - ast.Sub : '(__exprinfo_left - __exprinfo_right)', - ast.Mul : '(__exprinfo_left * __exprinfo_right)', - ast.Div : '(__exprinfo_left / __exprinfo_right)', - ast.Mod : '(__exprinfo_left % __exprinfo_right)', - ast.Power : '(__exprinfo_left ** __exprinfo_right)', - }.items(): - - class BinaryArith(Interpretable): - __view__ = astclass - - def eval(self, frame, astpattern=astpattern): - left = Interpretable(self.left) - left.eval(frame) - right = Interpretable(self.right) - right.eval(frame) - self.explanation = (astpattern - .replace('__exprinfo_left', left .explanation) - .replace('__exprinfo_right', right.explanation)) - try: - self.result = frame.eval(astpattern, - __exprinfo_left=left.result, - __exprinfo_right=right.result) - except passthroughex: - raise - except: - raise Failure(self) - - keepalive.append(BinaryArith) - - -class CallFunc(Interpretable): - __view__ = ast.CallFunc - - def is_bool(self, frame): - source = 'isinstance(__exprinfo_value, bool)' - try: - return frame.is_true(frame.eval(source, - __exprinfo_value=self.result)) - except passthroughex: - raise - except: - return False - - def eval(self, frame): - node = Interpretable(self.node) - node.eval(frame) - explanations = [] - vars = {'__exprinfo_fn': node.result} - source = '__exprinfo_fn(' - for a in self.args: - if isinstance(a, ast.Keyword): - keyword = a.name - a = a.expr - else: - keyword = None - a = Interpretable(a) - a.eval(frame) - argname = '__exprinfo_%d' % len(vars) - vars[argname] = a.result - if keyword is None: - source += argname + ',' - explanations.append(a.explanation) - else: - source += '%s=%s,' % (keyword, argname) - explanations.append('%s=%s' % (keyword, a.explanation)) - if self.star_args: - star_args = Interpretable(self.star_args) - star_args.eval(frame) - argname = '__exprinfo_star' - vars[argname] = star_args.result - source += '*' + argname + ',' - explanations.append('*' + star_args.explanation) - if self.dstar_args: - dstar_args = Interpretable(self.dstar_args) - dstar_args.eval(frame) - argname = '__exprinfo_kwds' - vars[argname] = dstar_args.result - source += '**' + argname + ',' - explanations.append('**' + dstar_args.explanation) - self.explanation = "%s(%s)" % ( - node.explanation, ', '.join(explanations)) - if source.endswith(','): - source = source[:-1] - source += ')' - try: - self.result = frame.eval(source, **vars) - except passthroughex: - raise - except: - raise Failure(self) - if not node.is_builtin(frame) or not self.is_bool(frame): - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -class Getattr(Interpretable): - __view__ = ast.Getattr - - def eval(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - source = '__exprinfo_expr.%s' % self.attrname - try: - self.result = frame.eval(source, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - self.explanation = '%s.%s' % (expr.explanation, self.attrname) - # if the attribute comes from the instance, its value is interesting - source = ('hasattr(__exprinfo_expr, "__dict__") and ' - '%r in __exprinfo_expr.__dict__' % self.attrname) - try: - from_instance = frame.is_true( - frame.eval(source, __exprinfo_expr=expr.result)) - except passthroughex: - raise - except: - from_instance = True - if from_instance: - r = frame.repr(self.result) - self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) - -# == Re-interpretation of full statements == - -class Assert(Interpretable): - __view__ = ast.Assert - - def run(self, frame): - test = Interpretable(self.test) - test.eval(frame) - # simplify 'assert False where False = ...' - if (test.explanation.startswith('False\n{False = ') and - test.explanation.endswith('\n}')): - test.explanation = test.explanation[15:-2] - # print the result as 'assert <explanation>' - self.result = test.result - self.explanation = 'assert ' + test.explanation - if not frame.is_true(test.result): - try: - raise BuiltinAssertionError - except passthroughex: - raise - except: - raise Failure(self) - -class Assign(Interpretable): - __view__ = ast.Assign - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = '... = ' + expr.explanation - # fall-back-run the rest of the assignment - ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr')) - mod = ast.Module(None, ast.Stmt([ass])) - mod.filename = '<run>' - co = pycodegen.ModuleCodeGenerator(mod).getCode() - try: - frame.exec_(co, __exprinfo_expr=expr.result) - except passthroughex: - raise - except: - raise Failure(self) - -class Discard(Interpretable): - __view__ = ast.Discard - - def run(self, frame): - expr = Interpretable(self.expr) - expr.eval(frame) - self.result = expr.result - self.explanation = expr.explanation - -class Stmt(Interpretable): - __view__ = ast.Stmt - - def run(self, frame): - for stmt in self.nodes: - stmt = Interpretable(stmt) - stmt.run(frame) - - -def report_failure(e): - explanation = e.node.nice_explanation() - if explanation: - explanation = ", in: " + explanation - else: - explanation = "" - sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) - -def check(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - expr = parse(s, 'eval') - assert isinstance(expr, ast.Expression) - node = Interpretable(expr.node) - try: - node.eval(frame) - except passthroughex: - raise - except Failure: - e = sys.exc_info()[1] - report_failure(e) - else: - if not frame.is_true(node.result): - sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) - - -########################################################### -# API / Entry points -# ######################################################### - -def interpret(source, frame, should_fail=False): - module = Interpretable(parse(source, 'exec').node) - #print "got module", module - if isinstance(frame, types.FrameType): - frame = py.code.Frame(frame) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - return getfailure(e) - except passthroughex: - raise - except: - import traceback - traceback.print_exc() - if should_fail: - return ("(assertion failed, but when it was re-run for " - "printing intermediate values, it did not fail. Suggestions: " - "compute assert expression before the assert or use --nomagic)") - else: - return None - -def getmsg(excinfo): - if isinstance(excinfo, tuple): - excinfo = py.code.ExceptionInfo(excinfo) - #frame, line = gettbline(tb) - #frame = py.code.Frame(frame) - #return interpret(line, frame) - - tb = excinfo.traceback[-1] - source = str(tb.statement).strip() - x = interpret(source, tb.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError("interpret returned non-string %r" % (x,)) - return x - -def getfailure(e): - explanation = e.node.nice_explanation() - if str(e.value): - lines = explanation.split('\n') - lines[0] += " << %s" % (e.value,) - explanation = '\n'.join(lines) - text = "%s: %s" % (e.exc.__name__, explanation) - if text.startswith('AssertionError: assert '): - text = text[16:] - return text - -def run(s, frame=None): - if frame is None: - frame = sys._getframe(1) - frame = py.code.Frame(frame) - module = Interpretable(parse(s, 'exec').node) - try: - module.run(frame) - except Failure: - e = sys.exc_info()[1] - report_failure(e) - - -if __name__ == '__main__': - # example: - def f(): - return 5 - def g(): - return 3 - def h(x): - return 'never' - check("f() * g() == 5") - check("not f()") - check("not (f() and g() or 0)") - check("f() == g()") - i = 4 - check("i == f()") - check("len(f()) == 0") - check("isinstance(2+3+4, float)") - - run("x = i") - check("x == 5") - - run("assert not f(), 'oops'") - run("a, b, c = 1, 2") - run("a, b, c = f()") - - check("max([f(),g()]) == 4") - check("'hello'[g()] == 'h'") - run("'guk%d' % h(f())") diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/_py2traceback.py b/tests/wpt/tests/tools/third_party/py/py/_code/_py2traceback.py deleted file mode 100644 index d65e27cb730..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/_py2traceback.py +++ /dev/null @@ -1,79 +0,0 @@ -# copied from python-2.7.3's traceback.py -# CHANGES: -# - some_str is replaced, trying to create unicode strings -# -import types - -def format_exception_only(etype, value): - """Format the exception part of a traceback. - - The arguments are the exception type and value such as given by - sys.last_type and sys.last_value. The return value is a list of - strings, each ending in a newline. - - Normally, the list contains a single string; however, for - SyntaxError exceptions, it contains several lines that (when - printed) display detailed information about where the syntax - error occurred. - - The message indicating which exception occurred is always the last - string in the list. - - """ - - # An instance should not have a meaningful value parameter, but - # sometimes does, particularly for string exceptions, such as - # >>> raise string1, string2 # deprecated - # - # Clear these out first because issubtype(string1, SyntaxError) - # would throw another exception and mask the original problem. - if (isinstance(etype, BaseException) or - isinstance(etype, types.InstanceType) or - etype is None or type(etype) is str): - return [_format_final_exc_line(etype, value)] - - stype = etype.__name__ - - if not issubclass(etype, SyntaxError): - return [_format_final_exc_line(stype, value)] - - # It was a syntax error; show exactly where the problem was found. - lines = [] - try: - msg, (filename, lineno, offset, badline) = value.args - except Exception: - pass - else: - filename = filename or "<string>" - lines.append(' File "%s", line %d\n' % (filename, lineno)) - if badline is not None: - lines.append(' %s\n' % badline.strip()) - if offset is not None: - caretspace = badline.rstrip('\n')[:offset].lstrip() - # non-space whitespace (likes tabs) must be kept for alignment - caretspace = ((c.isspace() and c or ' ') for c in caretspace) - # only three spaces to account for offset1 == pos 0 - lines.append(' %s^\n' % ''.join(caretspace)) - value = msg - - lines.append(_format_final_exc_line(stype, value)) - return lines - -def _format_final_exc_line(etype, value): - """Return a list of a single line -- normal case for format_exception_only""" - valuestr = _some_str(value) - if value is None or not valuestr: - line = "%s\n" % etype - else: - line = "%s: %s\n" % (etype, valuestr) - return line - -def _some_str(value): - try: - return unicode(value) - except Exception: - try: - return str(value) - except Exception: - pass - return '<unprintable %s object>' % type(value).__name__ diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/assertion.py b/tests/wpt/tests/tools/third_party/py/py/_code/assertion.py deleted file mode 100644 index ff1643799c9..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/assertion.py +++ /dev/null @@ -1,90 +0,0 @@ -import sys -import py - -BuiltinAssertionError = py.builtin.builtins.AssertionError - -_reprcompare = None # if set, will be called by assert reinterp for comparison ops - -def _format_explanation(explanation): - """This formats an explanation - - Normally all embedded newlines are escaped, however there are - three exceptions: \n{, \n} and \n~. The first two are intended - cover nested explanations, see function and attribute explanations - for examples (.visit_Call(), visit_Attribute()). The last one is - for when one explanation needs to span multiple lines, e.g. when - displaying diffs. - """ - raw_lines = (explanation or '').split('\n') - # escape newlines not followed by {, } and ~ - lines = [raw_lines[0]] - for l in raw_lines[1:]: - if l.startswith('{') or l.startswith('}') or l.startswith('~'): - lines.append(l) - else: - lines[-1] += '\\n' + l - - result = lines[:1] - stack = [0] - stackcnt = [0] - for line in lines[1:]: - if line.startswith('{'): - if stackcnt[-1]: - s = 'and ' - else: - s = 'where ' - stack.append(len(result)) - stackcnt[-1] += 1 - stackcnt.append(0) - result.append(' +' + ' '*(len(stack)-1) + s + line[1:]) - elif line.startswith('}'): - assert line.startswith('}') - stack.pop() - stackcnt.pop() - result[stack[-1]] += line[1:] - else: - assert line.startswith('~') - result.append(' '*len(stack) + line[1:]) - assert len(stack) == 1 - return '\n'.join(result) - - -class AssertionError(BuiltinAssertionError): - def __init__(self, *args): - BuiltinAssertionError.__init__(self, *args) - if args: - try: - self.msg = str(args[0]) - except py.builtin._sysex: - raise - except: - self.msg = "<[broken __repr__] %s at %0xd>" %( - args[0].__class__, id(args[0])) - else: - f = py.code.Frame(sys._getframe(1)) - try: - source = f.code.fullsource - if source is not None: - try: - source = source.getstatement(f.lineno, assertion=True) - except IndexError: - source = None - else: - source = str(source.deindent()).strip() - except py.error.ENOENT: - source = None - # this can also occur during reinterpretation, when the - # co_filename is set to "<run>". - if source: - self.msg = reinterpret(source, f, should_fail=True) - else: - self.msg = "<could not determine information>" - if not self.args: - self.args = (self.msg,) - -if sys.version_info > (3, 0): - AssertionError.__module__ = "builtins" - reinterpret_old = "old reinterpretation not available for py3" -else: - from py._code._assertionold import interpret as reinterpret_old -from py._code._assertionnew import interpret as reinterpret diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/code.py b/tests/wpt/tests/tools/third_party/py/py/_code/code.py deleted file mode 100644 index dad796283fe..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/code.py +++ /dev/null @@ -1,796 +0,0 @@ -import py -import sys -from inspect import CO_VARARGS, CO_VARKEYWORDS, isclass - -builtin_repr = repr - -reprlib = py.builtin._tryimport('repr', 'reprlib') - -if sys.version_info[0] >= 3: - from traceback import format_exception_only -else: - from py._code._py2traceback import format_exception_only - -import traceback - - -class Code(object): - """ wrapper around Python code objects """ - def __init__(self, rawcode): - if not hasattr(rawcode, "co_filename"): - rawcode = py.code.getrawcode(rawcode) - try: - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name - except AttributeError: - raise TypeError("not a code object: %r" % (rawcode,)) - self.raw = rawcode - - def __eq__(self, other): - return self.raw == other.raw - - def __ne__(self, other): - return not self == other - - @property - def path(self): - """ return a path object pointing to source code (note that it - might not point to an actually existing file). """ - p = py.path.local(self.raw.co_filename) - # maybe don't try this checking - if not p.check(): - # XXX maybe try harder like the weird logic - # in the standard lib [linecache.updatecache] does? - p = self.raw.co_filename - return p - - @property - def fullsource(self): - """ return a py.code.Source object for the full source file of the code - """ - from py._code import source - full, _ = source.findsource(self.raw) - return full - - def source(self): - """ return a py.code.Source object for the code object's source only - """ - # return source only for that part of code - return py.code.Source(self.raw) - - def getargs(self, var=False): - """ return a tuple with the argument names for the code object - - if 'var' is set True also return the names of the variable and - keyword arguments when present - """ - # handfull shortcut for getting args - raw = self.raw - argcount = raw.co_argcount - if var: - argcount += raw.co_flags & CO_VARARGS - argcount += raw.co_flags & CO_VARKEYWORDS - return raw.co_varnames[:argcount] - -class Frame(object): - """Wrapper around a Python frame holding f_locals and f_globals - in which expressions can be evaluated.""" - - def __init__(self, frame): - self.lineno = frame.f_lineno - 1 - self.f_globals = frame.f_globals - self.f_locals = frame.f_locals - self.raw = frame - self.code = py.code.Code(frame.f_code) - - @property - def statement(self): - """ statement this frame is at """ - if self.code.fullsource is None: - return py.code.Source("") - return self.code.fullsource.getstatement(self.lineno) - - def eval(self, code, **vars): - """ evaluate 'code' in the frame - - 'vars' are optional additional local variables - - returns the result of the evaluation - """ - f_locals = self.f_locals.copy() - f_locals.update(vars) - return eval(code, self.f_globals, f_locals) - - def exec_(self, code, **vars): - """ exec 'code' in the frame - - 'vars' are optiona; additional local variables - """ - f_locals = self.f_locals.copy() - f_locals.update(vars) - py.builtin.exec_(code, self.f_globals, f_locals) - - def repr(self, object): - """ return a 'safe' (non-recursive, one-line) string repr for 'object' - """ - return py.io.saferepr(object) - - def is_true(self, object): - return object - - def getargs(self, var=False): - """ return a list of tuples (name, value) for all arguments - - if 'var' is set True also include the variable and keyword - arguments when present - """ - retval = [] - for arg in self.code.getargs(var): - try: - retval.append((arg, self.f_locals[arg])) - except KeyError: - pass # this can occur when using Psyco - return retval - - -class TracebackEntry(object): - """ a single entry in a traceback """ - - _repr_style = None - exprinfo = None - - def __init__(self, rawentry): - self._rawentry = rawentry - self.lineno = rawentry.tb_lineno - 1 - - def set_repr_style(self, mode): - assert mode in ("short", "long") - self._repr_style = mode - - @property - def frame(self): - return py.code.Frame(self._rawentry.tb_frame) - - @property - def relline(self): - return self.lineno - self.frame.code.firstlineno - - def __repr__(self): - return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno+1) - - @property - def statement(self): - """ py.code.Source object for the current statement """ - source = self.frame.code.fullsource - return source.getstatement(self.lineno) - - @property - def path(self): - """ path to the source code """ - return self.frame.code.path - - def getlocals(self): - return self.frame.f_locals - locals = property(getlocals, None, None, "locals of underlaying frame") - - def reinterpret(self): - """Reinterpret the failing statement and returns a detailed information - about what operations are performed.""" - if self.exprinfo is None: - source = str(self.statement).strip() - x = py.code._reinterpret(source, self.frame, should_fail=True) - if not isinstance(x, str): - raise TypeError("interpret returned non-string %r" % (x,)) - self.exprinfo = x - return self.exprinfo - - def getfirstlinesource(self): - # on Jython this firstlineno can be -1 apparently - return max(self.frame.code.firstlineno, 0) - - def getsource(self, astcache=None): - """ return failing source code. """ - # we use the passed in astcache to not reparse asttrees - # within exception info printing - from py._code.source import getstatementrange_ast - source = self.frame.code.fullsource - if source is None: - return None - key = astnode = None - if astcache is not None: - key = self.frame.code.path - if key is not None: - astnode = astcache.get(key, None) - start = self.getfirstlinesource() - try: - astnode, _, end = getstatementrange_ast(self.lineno, source, - astnode=astnode) - except SyntaxError: - end = self.lineno + 1 - else: - if key is not None: - astcache[key] = astnode - return source[start:end] - - source = property(getsource) - - def ishidden(self): - """ return True if the current frame has a var __tracebackhide__ - resolving to True - - mostly for internal use - """ - try: - return self.frame.f_locals['__tracebackhide__'] - except KeyError: - try: - return self.frame.f_globals['__tracebackhide__'] - except KeyError: - return False - - def __str__(self): - try: - fn = str(self.path) - except py.error.Error: - fn = '???' - name = self.frame.code.name - try: - line = str(self.statement).lstrip() - except KeyboardInterrupt: - raise - except: - line = "???" - return " File %r:%d in %s\n %s\n" % (fn, self.lineno+1, name, line) - - def name(self): - return self.frame.code.raw.co_name - name = property(name, None, None, "co_name of underlaying code") - - -class Traceback(list): - """ Traceback objects encapsulate and offer higher level - access to Traceback entries. - """ - Entry = TracebackEntry - - def __init__(self, tb): - """ initialize from given python traceback object. """ - if hasattr(tb, 'tb_next'): - def f(cur): - while cur is not None: - yield self.Entry(cur) - cur = cur.tb_next - list.__init__(self, f(tb)) - else: - list.__init__(self, tb) - - def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): - """ return a Traceback instance wrapping part of this Traceback - - by provding any combination of path, lineno and firstlineno, the - first frame to start the to-be-returned traceback is determined - - this allows cutting the first part of a Traceback instance e.g. - for formatting reasons (removing some uninteresting bits that deal - with handling of the exception/traceback) - """ - for x in self: - code = x.frame.code - codepath = code.path - if ((path is None or codepath == path) and - (excludepath is None or not hasattr(codepath, 'relto') or - not codepath.relto(excludepath)) and - (lineno is None or x.lineno == lineno) and - (firstlineno is None or x.frame.code.firstlineno == firstlineno)): - return Traceback(x._rawentry) - return self - - def __getitem__(self, key): - val = super(Traceback, self).__getitem__(key) - if isinstance(key, type(slice(0))): - val = self.__class__(val) - return val - - def filter(self, fn=lambda x: not x.ishidden()): - """ return a Traceback instance with certain items removed - - fn is a function that gets a single argument, a TracebackItem - instance, and should return True when the item should be added - to the Traceback, False when not - - by default this removes all the TracebackItems which are hidden - (see ishidden() above) - """ - return Traceback(filter(fn, self)) - - def getcrashentry(self): - """ return last non-hidden traceback entry that lead - to the exception of a traceback. - """ - for i in range(-1, -len(self)-1, -1): - entry = self[i] - if not entry.ishidden(): - return entry - return self[-1] - - def recursionindex(self): - """ return the index of the frame/TracebackItem where recursion - originates if appropriate, None if no recursion occurred - """ - cache = {} - for i, entry in enumerate(self): - # id for the code.raw is needed to work around - # the strange metaprogramming in the decorator lib from pypi - # which generates code objects that have hash/value equality - #XXX needs a test - key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno - #print "checking for recursion at", key - l = cache.setdefault(key, []) - if l: - f = entry.frame - loc = f.f_locals - for otherloc in l: - if f.is_true(f.eval(co_equal, - __recursioncache_locals_1=loc, - __recursioncache_locals_2=otherloc)): - return i - l.append(entry.frame.f_locals) - return None - -co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2', - '?', 'eval') - -class ExceptionInfo(object): - """ wraps sys.exc_info() objects and offers - help for navigating the traceback. - """ - _striptext = '' - def __init__(self, tup=None, exprinfo=None): - if tup is None: - tup = sys.exc_info() - if exprinfo is None and isinstance(tup[1], AssertionError): - exprinfo = getattr(tup[1], 'msg', None) - if exprinfo is None: - exprinfo = str(tup[1]) - if exprinfo and exprinfo.startswith('assert '): - self._striptext = 'AssertionError: ' - self._excinfo = tup - #: the exception class - self.type = tup[0] - #: the exception instance - self.value = tup[1] - #: the exception raw traceback - self.tb = tup[2] - #: the exception type name - self.typename = self.type.__name__ - #: the exception traceback (py.code.Traceback instance) - self.traceback = py.code.Traceback(self.tb) - - def __repr__(self): - return "<ExceptionInfo %s tblen=%d>" % ( - self.typename, len(self.traceback)) - - def exconly(self, tryshort=False): - """ return the exception as a string - - when 'tryshort' resolves to True, and the exception is a - py.code._AssertionError, only the actual exception part of - the exception representation is returned (so 'AssertionError: ' is - removed from the beginning) - """ - lines = format_exception_only(self.type, self.value) - text = ''.join(lines) - text = text.rstrip() - if tryshort: - if text.startswith(self._striptext): - text = text[len(self._striptext):] - return text - - def errisinstance(self, exc): - """ return True if the exception is an instance of exc """ - return isinstance(self.value, exc) - - def _getreprcrash(self): - exconly = self.exconly(tryshort=True) - entry = self.traceback.getcrashentry() - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno+1, exconly) - - def getrepr(self, showlocals=False, style="long", - abspath=False, tbfilter=True, funcargs=False): - """ return str()able representation of this exception info. - showlocals: show locals per traceback entry - style: long|short|no|native traceback style - tbfilter: hide entries (where __tracebackhide__ is true) - - in case of style==native, tbfilter and showlocals is ignored. - """ - if style == 'native': - return ReprExceptionInfo(ReprTracebackNative( - traceback.format_exception( - self.type, - self.value, - self.traceback[0]._rawentry, - )), self._getreprcrash()) - - fmt = FormattedExcinfo( - showlocals=showlocals, style=style, - abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) - return fmt.repr_excinfo(self) - - def __str__(self): - entry = self.traceback[-1] - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return str(loc) - - def __unicode__(self): - entry = self.traceback[-1] - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return loc.__unicode__() - - -class FormattedExcinfo(object): - """ presenting information about failing Functions and Generators. """ - # for traceback entries - flow_marker = ">" - fail_marker = "E" - - def __init__(self, showlocals=False, style="long", - abspath=True, tbfilter=True, funcargs=False): - self.showlocals = showlocals - self.style = style - self.tbfilter = tbfilter - self.funcargs = funcargs - self.abspath = abspath - self.astcache = {} - - def _getindent(self, source): - # figure out indent for given source - try: - s = str(source.getstatement(len(source)-1)) - except KeyboardInterrupt: - raise - except: - try: - s = str(source[-1]) - except KeyboardInterrupt: - raise - except: - return 0 - return 4 + (len(s) - len(s.lstrip())) - - def _getentrysource(self, entry): - source = entry.getsource(self.astcache) - if source is not None: - source = source.deindent() - return source - - def _saferepr(self, obj): - return py.io.saferepr(obj) - - def repr_args(self, entry): - if self.funcargs: - args = [] - for argname, argvalue in entry.frame.getargs(var=True): - args.append((argname, self._saferepr(argvalue))) - return ReprFuncArgs(args) - - def get_source(self, source, line_index=-1, excinfo=None, short=False): - """ return formatted and marked up source lines. """ - lines = [] - if source is None or line_index >= len(source.lines): - source = py.code.Source("???") - line_index = 0 - if line_index < 0: - line_index += len(source) - space_prefix = " " - if short: - lines.append(space_prefix + source.lines[line_index].strip()) - else: - for line in source.lines[:line_index]: - lines.append(space_prefix + line) - lines.append(self.flow_marker + " " + source.lines[line_index]) - for line in source.lines[line_index+1:]: - lines.append(space_prefix + line) - if excinfo is not None: - indent = 4 if short else self._getindent(source) - lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) - return lines - - def get_exconly(self, excinfo, indent=4, markall=False): - lines = [] - indent = " " * indent - # get the real exception information out - exlines = excinfo.exconly(tryshort=True).split('\n') - failindent = self.fail_marker + indent[1:] - for line in exlines: - lines.append(failindent + line) - if not markall: - failindent = indent - return lines - - def repr_locals(self, locals): - if self.showlocals: - lines = [] - keys = [loc for loc in locals if loc[0] != "@"] - keys.sort() - for name in keys: - value = locals[name] - if name == '__builtins__': - lines.append("__builtins__ = <builtins>") - else: - # This formatting could all be handled by the - # _repr() function, which is only reprlib.Repr in - # disguise, so is very configurable. - str_repr = self._saferepr(value) - #if len(str_repr) < 70 or not isinstance(value, - # (list, tuple, dict)): - lines.append("%-10s = %s" %(name, str_repr)) - #else: - # self._line("%-10s =\\" % (name,)) - # # XXX - # pprint.pprint(value, stream=self.excinfowriter) - return ReprLocals(lines) - - def repr_traceback_entry(self, entry, excinfo=None): - source = self._getentrysource(entry) - if source is None: - source = py.code.Source("???") - line_index = 0 - else: - # entry.getfirstlinesource() can be -1, should be 0 on jython - line_index = entry.lineno - max(entry.getfirstlinesource(), 0) - - lines = [] - style = entry._repr_style - if style is None: - style = self.style - if style in ("short", "long"): - short = style == "short" - reprargs = self.repr_args(entry) if not short else None - s = self.get_source(source, line_index, excinfo, short=short) - lines.extend(s) - if short: - message = "in %s" %(entry.name) - else: - message = excinfo and excinfo.typename or "" - path = self._makepath(entry.path) - filelocrepr = ReprFileLocation(path, entry.lineno+1, message) - localsrepr = None - if not short: - localsrepr = self.repr_locals(entry.locals) - return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) - if excinfo: - lines.extend(self.get_exconly(excinfo, indent=4)) - return ReprEntry(lines, None, None, None, style) - - def _makepath(self, path): - if not self.abspath: - try: - np = py.path.local().bestrelpath(path) - except OSError: - return path - if len(np) < len(str(path)): - path = np - return path - - def repr_traceback(self, excinfo): - traceback = excinfo.traceback - if self.tbfilter: - traceback = traceback.filter() - recursionindex = None - if excinfo.errisinstance(RuntimeError): - if "maximum recursion depth exceeded" in str(excinfo.value): - recursionindex = traceback.recursionindex() - last = traceback[-1] - entries = [] - extraline = None - for index, entry in enumerate(traceback): - einfo = (last == entry) and excinfo or None - reprentry = self.repr_traceback_entry(entry, einfo) - entries.append(reprentry) - if index == recursionindex: - extraline = "!!! Recursion detected (same locals & position)" - break - return ReprTraceback(entries, extraline, style=self.style) - - def repr_excinfo(self, excinfo): - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() - return ReprExceptionInfo(reprtraceback, reprcrash) - -class TerminalRepr: - def __str__(self): - s = self.__unicode__() - if sys.version_info[0] < 3: - s = s.encode('utf-8') - return s - - def __unicode__(self): - # FYI this is called from pytest-xdist's serialization of exception - # information. - io = py.io.TextIO() - tw = py.io.TerminalWriter(file=io) - self.toterminal(tw) - return io.getvalue().strip() - - def __repr__(self): - return "<%s instance at %0x>" %(self.__class__, id(self)) - - -class ReprExceptionInfo(TerminalRepr): - def __init__(self, reprtraceback, reprcrash): - self.reprtraceback = reprtraceback - self.reprcrash = reprcrash - self.sections = [] - - def addsection(self, name, content, sep="-"): - self.sections.append((name, content, sep)) - - def toterminal(self, tw): - self.reprtraceback.toterminal(tw) - for name, content, sep in self.sections: - tw.sep(sep, name) - tw.line(content) - -class ReprTraceback(TerminalRepr): - entrysep = "_ " - - def __init__(self, reprentries, extraline, style): - self.reprentries = reprentries - self.extraline = extraline - self.style = style - - def toterminal(self, tw): - # the entries might have different styles - last_style = None - for i, entry in enumerate(self.reprentries): - if entry.style == "long": - tw.line("") - entry.toterminal(tw) - if i < len(self.reprentries) - 1: - next_entry = self.reprentries[i+1] - if entry.style == "long" or \ - entry.style == "short" and next_entry.style == "long": - tw.sep(self.entrysep) - - if self.extraline: - tw.line(self.extraline) - -class ReprTracebackNative(ReprTraceback): - def __init__(self, tblines): - self.style = "native" - self.reprentries = [ReprEntryNative(tblines)] - self.extraline = None - -class ReprEntryNative(TerminalRepr): - style = "native" - - def __init__(self, tblines): - self.lines = tblines - - def toterminal(self, tw): - tw.write("".join(self.lines)) - -class ReprEntry(TerminalRepr): - localssep = "_ " - - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): - self.lines = lines - self.reprfuncargs = reprfuncargs - self.reprlocals = reprlocals - self.reprfileloc = filelocrepr - self.style = style - - def toterminal(self, tw): - if self.style == "short": - self.reprfileloc.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) - #tw.line("") - return - if self.reprfuncargs: - self.reprfuncargs.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) - if self.reprlocals: - #tw.sep(self.localssep, "Locals") - tw.line("") - self.reprlocals.toterminal(tw) - if self.reprfileloc: - if self.lines: - tw.line("") - self.reprfileloc.toterminal(tw) - - def __str__(self): - return "%s\n%s\n%s" % ("\n".join(self.lines), - self.reprlocals, - self.reprfileloc) - -class ReprFileLocation(TerminalRepr): - def __init__(self, path, lineno, message): - self.path = str(path) - self.lineno = lineno - self.message = message - - def toterminal(self, tw): - # filename and lineno output for each entry, - # using an output format that most editors unterstand - msg = self.message - i = msg.find("\n") - if i != -1: - msg = msg[:i] - tw.line("%s:%s: %s" %(self.path, self.lineno, msg)) - -class ReprLocals(TerminalRepr): - def __init__(self, lines): - self.lines = lines - - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - -class ReprFuncArgs(TerminalRepr): - def __init__(self, args): - self.args = args - - def toterminal(self, tw): - if self.args: - linesofar = "" - for name, value in self.args: - ns = "%s = %s" %(name, value) - if len(ns) + len(linesofar) + 2 > tw.fullwidth: - if linesofar: - tw.line(linesofar) - linesofar = ns - else: - if linesofar: - linesofar += ", " + ns - else: - linesofar = ns - if linesofar: - tw.line(linesofar) - tw.line("") - - - -oldbuiltins = {} - -def patch_builtins(assertion=True, compile=True): - """ put compile and AssertionError builtins to Python's builtins. """ - if assertion: - from py._code import assertion - l = oldbuiltins.setdefault('AssertionError', []) - l.append(py.builtin.builtins.AssertionError) - py.builtin.builtins.AssertionError = assertion.AssertionError - if compile: - l = oldbuiltins.setdefault('compile', []) - l.append(py.builtin.builtins.compile) - py.builtin.builtins.compile = py.code.compile - -def unpatch_builtins(assertion=True, compile=True): - """ remove compile and AssertionError builtins from Python builtins. """ - if assertion: - py.builtin.builtins.AssertionError = oldbuiltins['AssertionError'].pop() - if compile: - py.builtin.builtins.compile = oldbuiltins['compile'].pop() - -def getrawcode(obj, trycall=True): - """ return code object for given function. """ - try: - return obj.__code__ - except AttributeError: - obj = getattr(obj, 'im_func', obj) - obj = getattr(obj, 'func_code', obj) - obj = getattr(obj, 'f_code', obj) - obj = getattr(obj, '__code__', obj) - if trycall and not hasattr(obj, 'co_firstlineno'): - if hasattr(obj, '__call__') and not isclass(obj): - x = getrawcode(obj.__call__, trycall=False) - if hasattr(x, 'co_firstlineno'): - return x - return obj - diff --git a/tests/wpt/tests/tools/third_party/py/py/_code/source.py b/tests/wpt/tests/tools/third_party/py/py/_code/source.py deleted file mode 100644 index 7fc7b23a96c..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_code/source.py +++ /dev/null @@ -1,410 +0,0 @@ -from __future__ import generators - -from bisect import bisect_right -import sys -import inspect, tokenize -import py -from types import ModuleType -cpy_compile = compile - -try: - import _ast - from _ast import PyCF_ONLY_AST as _AST_FLAG -except ImportError: - _AST_FLAG = 0 - _ast = None - - -class Source(object): - """ a immutable object holding a source code fragment, - possibly deindenting it. - """ - _compilecounter = 0 - def __init__(self, *parts, **kwargs): - self.lines = lines = [] - de = kwargs.get('deindent', True) - rstrip = kwargs.get('rstrip', True) - for part in parts: - if not part: - partlines = [] - if isinstance(part, Source): - partlines = part.lines - elif isinstance(part, (tuple, list)): - partlines = [x.rstrip("\n") for x in part] - elif isinstance(part, py.builtin._basestring): - partlines = part.split('\n') - if rstrip: - while partlines: - if partlines[-1].strip(): - break - partlines.pop() - else: - partlines = getsource(part, deindent=de).lines - if de: - partlines = deindent(partlines) - lines.extend(partlines) - - def __eq__(self, other): - try: - return self.lines == other.lines - except AttributeError: - if isinstance(other, str): - return str(self) == other - return False - - def __getitem__(self, key): - if isinstance(key, int): - return self.lines[key] - else: - if key.step not in (None, 1): - raise IndexError("cannot slice a Source with a step") - return self.__getslice__(key.start, key.stop) - - def __len__(self): - return len(self.lines) - - def __getslice__(self, start, end): - newsource = Source() - newsource.lines = self.lines[start:end] - return newsource - - def strip(self): - """ return new source object with trailing - and leading blank lines removed. - """ - start, end = 0, len(self) - while start < end and not self.lines[start].strip(): - start += 1 - while end > start and not self.lines[end-1].strip(): - end -= 1 - source = Source() - source.lines[:] = self.lines[start:end] - return source - - def putaround(self, before='', after='', indent=' ' * 4): - """ return a copy of the source object with - 'before' and 'after' wrapped around it. - """ - before = Source(before) - after = Source(after) - newsource = Source() - lines = [ (indent + line) for line in self.lines] - newsource.lines = before.lines + lines + after.lines - return newsource - - def indent(self, indent=' ' * 4): - """ return a copy of the source object with - all lines indented by the given indent-string. - """ - newsource = Source() - newsource.lines = [(indent+line) for line in self.lines] - return newsource - - def getstatement(self, lineno, assertion=False): - """ return Source statement which contains the - given linenumber (counted from 0). - """ - start, end = self.getstatementrange(lineno, assertion) - return self[start:end] - - def getstatementrange(self, lineno, assertion=False): - """ return (start, end) tuple which spans the minimal - statement region which containing the given lineno. - """ - if not (0 <= lineno < len(self)): - raise IndexError("lineno out of range") - ast, start, end = getstatementrange_ast(lineno, self) - return start, end - - def deindent(self, offset=None): - """ return a new source object deindented by offset. - If offset is None then guess an indentation offset from - the first non-blank line. Subsequent lines which have a - lower indentation offset will be copied verbatim as - they are assumed to be part of multilines. - """ - # XXX maybe use the tokenizer to properly handle multiline - # strings etc.pp? - newsource = Source() - newsource.lines[:] = deindent(self.lines, offset) - return newsource - - def isparseable(self, deindent=True): - """ return True if source is parseable, heuristically - deindenting it by default. - """ - try: - import parser - except ImportError: - syntax_checker = lambda x: compile(x, 'asd', 'exec') - else: - syntax_checker = parser.suite - - if deindent: - source = str(self.deindent()) - else: - source = str(self) - try: - #compile(source+'\n', "x", "exec") - syntax_checker(source+'\n') - except KeyboardInterrupt: - raise - except Exception: - return False - else: - return True - - def __str__(self): - return "\n".join(self.lines) - - def compile(self, filename=None, mode='exec', - flag=generators.compiler_flag, - dont_inherit=0, _genframe=None): - """ return compiled code object. if filename is None - invent an artificial filename which displays - the source/line position of the caller frame. - """ - if not filename or py.path.local(filename).check(file=0): - if _genframe is None: - _genframe = sys._getframe(1) # the caller - fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno - base = "<%d-codegen " % self._compilecounter - self.__class__._compilecounter += 1 - if not filename: - filename = base + '%s:%d>' % (fn, lineno) - else: - filename = base + '%r %s:%d>' % (filename, fn, lineno) - source = "\n".join(self.lines) + '\n' - try: - co = cpy_compile(source, filename, mode, flag) - except SyntaxError: - ex = sys.exc_info()[1] - # re-represent syntax errors from parsing python strings - msglines = self.lines[:ex.lineno] - if ex.offset: - msglines.append(" "*ex.offset + '^') - msglines.append("(code was compiled probably from here: %s)" % filename) - newex = SyntaxError('\n'.join(msglines)) - newex.offset = ex.offset - newex.lineno = ex.lineno - newex.text = ex.text - raise newex - else: - if flag & _AST_FLAG: - return co - lines = [(x + "\n") for x in self.lines] - import linecache - linecache.cache[filename] = (1, None, lines, filename) - return co - -# -# public API shortcut functions -# - -def compile_(source, filename=None, mode='exec', flags= - generators.compiler_flag, dont_inherit=0): - """ compile the given source to a raw code object, - and maintain an internal cache which allows later - retrieval of the source code for the code object - and any recursively created code objects. - """ - if _ast is not None and isinstance(source, _ast.AST): - # XXX should Source support having AST? - return cpy_compile(source, filename, mode, flags, dont_inherit) - _genframe = sys._getframe(1) # the caller - s = Source(source) - co = s.compile(filename, mode, flags, _genframe=_genframe) - return co - - -def getfslineno(obj): - """ Return source location (path, lineno) for the given object. - If the source cannot be determined return ("", -1) - """ - try: - code = py.code.Code(obj) - except TypeError: - try: - fn = (inspect.getsourcefile(obj) or - inspect.getfile(obj)) - except TypeError: - return "", -1 - - fspath = fn and py.path.local(fn) or None - lineno = -1 - if fspath: - try: - _, lineno = findsource(obj) - except IOError: - pass - else: - fspath = code.path - lineno = code.firstlineno - assert isinstance(lineno, int) - return fspath, lineno - -# -# helper functions -# - -def findsource(obj): - try: - sourcelines, lineno = inspect.findsource(obj) - except py.builtin._sysex: - raise - except: - return None, -1 - source = Source() - source.lines = [line.rstrip() for line in sourcelines] - return source, lineno - -def getsource(obj, **kwargs): - obj = py.code.getrawcode(obj) - try: - strsrc = inspect.getsource(obj) - except IndentationError: - strsrc = "\"Buggy python version consider upgrading, cannot get source\"" - assert isinstance(strsrc, str) - return Source(strsrc, **kwargs) - -def deindent(lines, offset=None): - if offset is None: - for line in lines: - line = line.expandtabs() - s = line.lstrip() - if s: - offset = len(line)-len(s) - break - else: - offset = 0 - if offset == 0: - return list(lines) - newlines = [] - def readline_generator(lines): - for line in lines: - yield line + '\n' - while True: - yield '' - - it = readline_generator(lines) - - try: - for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)): - if sline > len(lines): - break # End of input reached - if sline > len(newlines): - line = lines[sline - 1].expandtabs() - if line.lstrip() and line[:offset].isspace(): - line = line[offset:] # Deindent - newlines.append(line) - - for i in range(sline, eline): - # Don't deindent continuing lines of - # multiline tokens (i.e. multiline strings) - newlines.append(lines[i]) - except (IndentationError, tokenize.TokenError): - pass - # Add any lines we didn't see. E.g. if an exception was raised. - newlines.extend(lines[len(newlines):]) - return newlines - - -def get_statement_startend2(lineno, node): - import ast - # flatten all statements and except handlers into one lineno-list - # AST's line numbers start indexing at 1 - l = [] - for x in ast.walk(node): - if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler): - l.append(x.lineno - 1) - for name in "finalbody", "orelse": - val = getattr(x, name, None) - if val: - # treat the finally/orelse part as its own statement - l.append(val[0].lineno - 1 - 1) - l.sort() - insert_index = bisect_right(l, lineno) - start = l[insert_index - 1] - if insert_index >= len(l): - end = None - else: - end = l[insert_index] - return start, end - - -def getstatementrange_ast(lineno, source, assertion=False, astnode=None): - if astnode is None: - content = str(source) - try: - astnode = compile(content, "source", "exec", 1024) # 1024 for AST - except ValueError: - start, end = getstatementrange_old(lineno, source, assertion) - return None, start, end - start, end = get_statement_startend2(lineno, astnode) - # we need to correct the end: - # - ast-parsing strips comments - # - there might be empty lines - # - we might have lesser indented code blocks at the end - if end is None: - end = len(source.lines) - - if end > start + 1: - # make sure we don't span differently indented code blocks - # by using the BlockFinder helper used which inspect.getsource() uses itself - block_finder = inspect.BlockFinder() - # if we start with an indented line, put blockfinder to "started" mode - block_finder.started = source.lines[start][0].isspace() - it = ((x + "\n") for x in source.lines[start:end]) - try: - for tok in tokenize.generate_tokens(lambda: next(it)): - block_finder.tokeneater(*tok) - except (inspect.EndOfBlock, IndentationError): - end = block_finder.last + start - except Exception: - pass - - # the end might still point to a comment or empty line, correct it - while end: - line = source.lines[end - 1].lstrip() - if line.startswith("#") or not line: - end -= 1 - else: - break - return astnode, start, end - - -def getstatementrange_old(lineno, source, assertion=False): - """ return (start, end) tuple which spans the minimal - statement region which containing the given lineno. - raise an IndexError if no such statementrange can be found. - """ - # XXX this logic is only used on python2.4 and below - # 1. find the start of the statement - from codeop import compile_command - for start in range(lineno, -1, -1): - if assertion: - line = source.lines[start] - # the following lines are not fully tested, change with care - if 'super' in line and 'self' in line and '__init__' in line: - raise IndexError("likely a subclass") - if "assert" not in line and "raise" not in line: - continue - trylines = source.lines[start:lineno+1] - # quick hack to prepare parsing an indented line with - # compile_command() (which errors on "return" outside defs) - trylines.insert(0, 'def xxx():') - trysource = '\n '.join(trylines) - # ^ space here - try: - compile_command(trysource) - except (SyntaxError, OverflowError, ValueError): - continue - - # 2. find the end of the statement - for end in range(lineno+1, len(source)+1): - trysource = source[start:end] - if trysource.isparseable(): - return start, end - raise SyntaxError("no valid source range around line %d " % (lineno,)) - - diff --git a/tests/wpt/tests/tools/third_party/py/py/_error.py b/tests/wpt/tests/tools/third_party/py/py/_error.py deleted file mode 100644 index a6375de9fa2..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_error.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -create errno-specific classes for IO or os calls. - -""" -from types import ModuleType -import sys, os, errno - -class Error(EnvironmentError): - def __repr__(self): - return "%s.%s %r: %s " %(self.__class__.__module__, - self.__class__.__name__, - self.__class__.__doc__, - " ".join(map(str, self.args)), - #repr(self.args) - ) - - def __str__(self): - s = "[%s]: %s" %(self.__class__.__doc__, - " ".join(map(str, self.args)), - ) - return s - -_winerrnomap = { - 2: errno.ENOENT, - 3: errno.ENOENT, - 17: errno.EEXIST, - 18: errno.EXDEV, - 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable - 22: errno.ENOTDIR, - 20: errno.ENOTDIR, - 267: errno.ENOTDIR, - 5: errno.EACCES, # anything better? -} - -class ErrorMaker(ModuleType): - """ lazily provides Exception classes for each possible POSIX errno - (as defined per the 'errno' module). All such instances - subclass EnvironmentError. - """ - Error = Error - _errno2class = {} - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) - eno = getattr(errno, name) - cls = self._geterrnoclass(eno) - setattr(self, name, cls) - return cls - - def _geterrnoclass(self, eno): - try: - return self._errno2class[eno] - except KeyError: - clsname = errno.errorcode.get(eno, "UnknownErrno%d" %(eno,)) - errorcls = type(Error)(clsname, (Error,), - {'__module__':'py.error', - '__doc__': os.strerror(eno)}) - self._errno2class[eno] = errorcls - return errorcls - - def checked_call(self, func, *args, **kwargs): - """ call a function and raise an errno-exception if applicable. """ - __tracebackhide__ = True - try: - return func(*args, **kwargs) - except self.Error: - raise - except (OSError, EnvironmentError): - cls, value, tb = sys.exc_info() - if not hasattr(value, 'errno'): - raise - __tracebackhide__ = False - errno = value.errno - try: - if not isinstance(value, WindowsError): - raise NameError - except NameError: - # we are not on Windows, or we got a proper OSError - cls = self._geterrnoclass(errno) - else: - try: - cls = self._geterrnoclass(_winerrnomap[errno]) - except KeyError: - raise value - raise cls("%s%r" % (func.__name__, args)) - __tracebackhide__ = True - - -error = ErrorMaker('py.error') -sys.modules[error.__name__] = error
\ No newline at end of file diff --git a/tests/wpt/tests/tools/third_party/py/py/_io/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_io/__init__.py deleted file mode 100644 index 835f01f3ab9..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_io/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" input/output helping """ diff --git a/tests/wpt/tests/tools/third_party/py/py/_io/capture.py b/tests/wpt/tests/tools/third_party/py/py/_io/capture.py deleted file mode 100644 index cacf2fa71a1..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_io/capture.py +++ /dev/null @@ -1,371 +0,0 @@ -import os -import sys -import py -import tempfile - -try: - from io import StringIO -except ImportError: - from StringIO import StringIO - -if sys.version_info < (3,0): - class TextIO(StringIO): - def write(self, data): - if not isinstance(data, unicode): - data = unicode(data, getattr(self, '_encoding', 'UTF-8'), 'replace') - return StringIO.write(self, data) -else: - TextIO = StringIO - -try: - from io import BytesIO -except ImportError: - class BytesIO(StringIO): - def write(self, data): - if isinstance(data, unicode): - raise TypeError("not a byte value: %r" %(data,)) - return StringIO.write(self, data) - -patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} - -class FDCapture: - """ Capture IO to/from a given os-level filedescriptor. """ - - def __init__(self, targetfd, tmpfile=None, now=True, patchsys=False): - """ save targetfd descriptor, and open a new - temporary file there. If no tmpfile is - specified a tempfile.Tempfile() will be opened - in text mode. - """ - self.targetfd = targetfd - if tmpfile is None and targetfd != 0: - f = tempfile.TemporaryFile('wb+') - tmpfile = dupfile(f, encoding="UTF-8") - f.close() - self.tmpfile = tmpfile - self._savefd = os.dup(self.targetfd) - if patchsys: - self._oldsys = getattr(sys, patchsysdict[targetfd]) - if now: - self.start() - - def start(self): - try: - os.fstat(self._savefd) - except OSError: - raise ValueError("saved filedescriptor not valid, " - "did you call start() twice?") - if self.targetfd == 0 and not self.tmpfile: - fd = os.open(devnullpath, os.O_RDONLY) - os.dup2(fd, 0) - os.close(fd) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], DontReadFromInput()) - else: - os.dup2(self.tmpfile.fileno(), self.targetfd) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], self.tmpfile) - - def done(self): - """ unpatch and clean up, returns the self.tmpfile (file object) - """ - os.dup2(self._savefd, self.targetfd) - os.close(self._savefd) - if self.targetfd != 0: - self.tmpfile.seek(0) - if hasattr(self, '_oldsys'): - setattr(sys, patchsysdict[self.targetfd], self._oldsys) - return self.tmpfile - - def writeorg(self, data): - """ write a string to the original file descriptor - """ - tempfp = tempfile.TemporaryFile() - try: - os.dup2(self._savefd, tempfp.fileno()) - tempfp.write(data) - finally: - tempfp.close() - - -def dupfile(f, mode=None, buffering=0, raising=False, encoding=None): - """ return a new open file object that's a duplicate of f - - mode is duplicated if not given, 'buffering' controls - buffer size (defaulting to no buffering) and 'raising' - defines whether an exception is raised when an incompatible - file object is passed in (if raising is False, the file - object itself will be returned) - """ - try: - fd = f.fileno() - mode = mode or f.mode - except AttributeError: - if raising: - raise - return f - newfd = os.dup(fd) - if sys.version_info >= (3,0): - if encoding is not None: - mode = mode.replace("b", "") - buffering = True - return os.fdopen(newfd, mode, buffering, encoding, closefd=True) - else: - f = os.fdopen(newfd, mode, buffering) - if encoding is not None: - return EncodedFile(f, encoding) - return f - -class EncodedFile(object): - def __init__(self, _stream, encoding): - self._stream = _stream - self.encoding = encoding - - def write(self, obj): - if isinstance(obj, unicode): - obj = obj.encode(self.encoding) - elif isinstance(obj, str): - pass - else: - obj = str(obj) - self._stream.write(obj) - - def writelines(self, linelist): - data = ''.join(linelist) - self.write(data) - - def __getattr__(self, name): - return getattr(self._stream, name) - -class Capture(object): - def call(cls, func, *args, **kwargs): - """ return a (res, out, err) tuple where - out and err represent the output/error output - during function execution. - call the given function with args/kwargs - and capture output/error during its execution. - """ - so = cls() - try: - res = func(*args, **kwargs) - finally: - out, err = so.reset() - return res, out, err - call = classmethod(call) - - def reset(self): - """ reset sys.stdout/stderr and return captured output as strings. """ - if hasattr(self, '_reset'): - raise ValueError("was already reset") - self._reset = True - outfile, errfile = self.done(save=False) - out, err = "", "" - if outfile and not outfile.closed: - out = outfile.read() - outfile.close() - if errfile and errfile != outfile and not errfile.closed: - err = errfile.read() - errfile.close() - return out, err - - def suspend(self): - """ return current snapshot captures, memorize tempfiles. """ - outerr = self.readouterr() - outfile, errfile = self.done() - return outerr - - -class StdCaptureFD(Capture): - """ This class allows to capture writes to FD1 and FD2 - and may connect a NULL file to FD0 (and prevent - reads from sys.stdin). If any of the 0,1,2 file descriptors - is invalid it will not be captured. - """ - def __init__(self, out=True, err=True, mixed=False, - in_=True, patchsys=True, now=True): - self._options = { - "out": out, - "err": err, - "mixed": mixed, - "in_": in_, - "patchsys": patchsys, - "now": now, - } - self._save() - if now: - self.startall() - - def _save(self): - in_ = self._options['in_'] - out = self._options['out'] - err = self._options['err'] - mixed = self._options['mixed'] - patchsys = self._options['patchsys'] - if in_: - try: - self.in_ = FDCapture(0, tmpfile=None, now=False, - patchsys=patchsys) - except OSError: - pass - if out: - tmpfile = None - if hasattr(out, 'write'): - tmpfile = out - try: - self.out = FDCapture(1, tmpfile=tmpfile, - now=False, patchsys=patchsys) - self._options['out'] = self.out.tmpfile - except OSError: - pass - if err: - if out and mixed: - tmpfile = self.out.tmpfile - elif hasattr(err, 'write'): - tmpfile = err - else: - tmpfile = None - try: - self.err = FDCapture(2, tmpfile=tmpfile, - now=False, patchsys=patchsys) - self._options['err'] = self.err.tmpfile - except OSError: - pass - - def startall(self): - if hasattr(self, 'in_'): - self.in_.start() - if hasattr(self, 'out'): - self.out.start() - if hasattr(self, 'err'): - self.err.start() - - def resume(self): - """ resume capturing with original temp files. """ - self.startall() - - def done(self, save=True): - """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = None - if hasattr(self, 'out') and not self.out.tmpfile.closed: - outfile = self.out.done() - if hasattr(self, 'err') and not self.err.tmpfile.closed: - errfile = self.err.done() - if hasattr(self, 'in_'): - tmpfile = self.in_.done() - if save: - self._save() - return outfile, errfile - - def readouterr(self): - """ return snapshot value of stdout/stderr capturings. """ - if hasattr(self, "out"): - out = self._readsnapshot(self.out.tmpfile) - else: - out = "" - if hasattr(self, "err"): - err = self._readsnapshot(self.err.tmpfile) - else: - err = "" - return out, err - - def _readsnapshot(self, f): - f.seek(0) - res = f.read() - enc = getattr(f, "encoding", None) - if enc: - res = py.builtin._totext(res, enc, "replace") - f.truncate(0) - f.seek(0) - return res - - -class StdCapture(Capture): - """ This class allows to capture writes to sys.stdout|stderr "in-memory" - and will raise errors on tries to read from sys.stdin. It only - modifies sys.stdout|stderr|stdin attributes and does not - touch underlying File Descriptors (use StdCaptureFD for that). - """ - def __init__(self, out=True, err=True, in_=True, mixed=False, now=True): - self._oldout = sys.stdout - self._olderr = sys.stderr - self._oldin = sys.stdin - if out and not hasattr(out, 'file'): - out = TextIO() - self.out = out - if err: - if mixed: - err = out - elif not hasattr(err, 'write'): - err = TextIO() - self.err = err - self.in_ = in_ - if now: - self.startall() - - def startall(self): - if self.out: - sys.stdout = self.out - if self.err: - sys.stderr = self.err - if self.in_: - sys.stdin = self.in_ = DontReadFromInput() - - def done(self, save=True): - """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = None - if self.out and not self.out.closed: - sys.stdout = self._oldout - outfile = self.out - outfile.seek(0) - if self.err and not self.err.closed: - sys.stderr = self._olderr - errfile = self.err - errfile.seek(0) - if self.in_: - sys.stdin = self._oldin - return outfile, errfile - - def resume(self): - """ resume capturing with original temp files. """ - self.startall() - - def readouterr(self): - """ return snapshot value of stdout/stderr capturings. """ - out = err = "" - if self.out: - out = self.out.getvalue() - self.out.truncate(0) - self.out.seek(0) - if self.err: - err = self.err.getvalue() - self.err.truncate(0) - self.err.seek(0) - return out, err - -class DontReadFromInput: - """Temporary stub class. Ideally when stdin is accessed, the - capturing should be turned off, with possibly all data captured - so far sent to the screen. This should be configurable, though, - because in automated test runs it is better to crash than - hang indefinitely. - """ - def read(self, *args): - raise IOError("reading from stdin while output is captured") - readline = read - readlines = read - __iter__ = read - - def fileno(self): - raise ValueError("redirected Stdin is pseudofile, has no fileno()") - def isatty(self): - return False - def close(self): - pass - -try: - devnullpath = os.devnull -except AttributeError: - if os.name == 'nt': - devnullpath = 'NUL' - else: - devnullpath = '/dev/null' diff --git a/tests/wpt/tests/tools/third_party/py/py/_io/saferepr.py b/tests/wpt/tests/tools/third_party/py/py/_io/saferepr.py deleted file mode 100644 index 8518290efdd..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_io/saferepr.py +++ /dev/null @@ -1,71 +0,0 @@ -import py -import sys - -builtin_repr = repr - -reprlib = py.builtin._tryimport('repr', 'reprlib') - -class SafeRepr(reprlib.Repr): - """ subclass of repr.Repr that limits the resulting size of repr() - and includes information on exceptions raised during the call. - """ - def repr(self, x): - return self._callhelper(reprlib.Repr.repr, self, x) - - def repr_unicode(self, x, level): - # Strictly speaking wrong on narrow builds - def repr(u): - if "'" not in u: - return py.builtin._totext("'%s'") % u - elif '"' not in u: - return py.builtin._totext('"%s"') % u - else: - return py.builtin._totext("'%s'") % u.replace("'", r"\'") - s = repr(x[:self.maxstring]) - if len(s) > self.maxstring: - i = max(0, (self.maxstring-3)//2) - j = max(0, self.maxstring-3-i) - s = repr(x[:i] + x[len(x)-j:]) - s = s[:i] + '...' + s[len(s)-j:] - return s - - def repr_instance(self, x, level): - return self._callhelper(builtin_repr, x) - - def _callhelper(self, call, x, *args): - try: - # Try the vanilla repr and make sure that the result is a string - s = call(x, *args) - except py.builtin._sysex: - raise - except: - cls, e, tb = sys.exc_info() - exc_name = getattr(cls, '__name__', 'unknown') - try: - exc_info = str(e) - except py.builtin._sysex: - raise - except: - exc_info = 'unknown' - return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( - exc_name, exc_info, x.__class__.__name__, id(x)) - else: - if len(s) > self.maxsize: - i = max(0, (self.maxsize-3)//2) - j = max(0, self.maxsize-3-i) - s = s[:i] + '...' + s[len(s)-j:] - return s - -def saferepr(obj, maxsize=240): - """ return a size-limited safe repr-string for the given object. - Failing __repr__ functions of user instances will be represented - with a short exception info and 'saferepr' generally takes - care to never raise exceptions itself. This function is a wrapper - around the Repr/reprlib functionality of the standard 2.6 lib. - """ - # review exception handling - srepr = SafeRepr() - srepr.maxstring = maxsize - srepr.maxsize = maxsize - srepr.maxother = 160 - return srepr.repr(obj) diff --git a/tests/wpt/tests/tools/third_party/py/py/_io/terminalwriter.py b/tests/wpt/tests/tools/third_party/py/py/_io/terminalwriter.py deleted file mode 100644 index 442ca2395e0..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_io/terminalwriter.py +++ /dev/null @@ -1,423 +0,0 @@ -""" - -Helper functions for writing to terminals and files. - -""" - - -import sys, os, unicodedata -import py -py3k = sys.version_info[0] >= 3 -py33 = sys.version_info >= (3, 3) -from py.builtin import text, bytes - -win32_and_ctypes = False -colorama = None -if sys.platform == "win32": - try: - import colorama - except ImportError: - try: - import ctypes - win32_and_ctypes = True - except ImportError: - pass - - -def _getdimensions(): - if py33: - import shutil - size = shutil.get_terminal_size() - return size.lines, size.columns - else: - import termios, fcntl, struct - call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) - height, width = struct.unpack("hhhh", call)[:2] - return height, width - - -def get_terminal_width(): - width = 0 - try: - _, width = _getdimensions() - except py.builtin._sysex: - raise - except: - # pass to fallback below - pass - - if width == 0: - # FALLBACK: - # * some exception happened - # * or this is emacs terminal which reports (0,0) - width = int(os.environ.get('COLUMNS', 80)) - - # XXX the windows getdimensions may be bogus, let's sanify a bit - if width < 40: - width = 80 - return width - -terminal_width = get_terminal_width() - -char_width = { - 'A': 1, # "Ambiguous" - 'F': 2, # Fullwidth - 'H': 1, # Halfwidth - 'N': 1, # Neutral - 'Na': 1, # Narrow - 'W': 2, # Wide -} - - -def get_line_width(text): - text = unicodedata.normalize('NFC', text) - return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) - - -# XXX unify with _escaped func below -def ansi_print(text, esc, file=None, newline=True, flush=False): - if file is None: - file = sys.stderr - text = text.rstrip() - if esc and not isinstance(esc, tuple): - esc = (esc,) - if esc and sys.platform != "win32" and file.isatty(): - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text + - '\x1b[0m') # ANSI color code "reset" - if newline: - text += '\n' - - if esc and win32_and_ctypes and file.isatty(): - if 1 in esc: - bold = True - esc = tuple([x for x in esc if x != 1]) - else: - bold = False - esctable = {() : FOREGROUND_WHITE, # normal - (31,): FOREGROUND_RED, # red - (32,): FOREGROUND_GREEN, # green - (33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow - (34,): FOREGROUND_BLUE, # blue - (35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple - (36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan - (37,): FOREGROUND_WHITE, # white - (39,): FOREGROUND_WHITE, # reset - } - attr = esctable.get(esc, FOREGROUND_WHITE) - if bold: - attr |= FOREGROUND_INTENSITY - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - if file is sys.stderr: - handle = GetStdHandle(STD_ERROR_HANDLE) - else: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - attr |= (oldcolors & 0x0f0) - SetConsoleTextAttribute(handle, attr) - while len(text) > 32768: - file.write(text[:32768]) - text = text[32768:] - if text: - file.write(text) - SetConsoleTextAttribute(handle, oldcolors) - else: - file.write(text) - - if flush: - file.flush() - -def should_do_markup(file): - if os.environ.get('PY_COLORS') == '1': - return True - if os.environ.get('PY_COLORS') == '0': - return False - if 'NO_COLOR' in os.environ: - return False - return hasattr(file, 'isatty') and file.isatty() \ - and os.environ.get('TERM') != 'dumb' \ - and not (sys.platform.startswith('java') and os._name == 'nt') - -class TerminalWriter(object): - _esctable = dict(black=30, red=31, green=32, yellow=33, - blue=34, purple=35, cyan=36, white=37, - Black=40, Red=41, Green=42, Yellow=43, - Blue=44, Purple=45, Cyan=46, White=47, - bold=1, light=2, blink=5, invert=7) - - # XXX deprecate stringio argument - def __init__(self, file=None, stringio=False, encoding=None): - if file is None: - if stringio: - self.stringio = file = py.io.TextIO() - else: - from sys import stdout as file - elif py.builtin.callable(file) and not ( - hasattr(file, "write") and hasattr(file, "flush")): - file = WriteFile(file, encoding=encoding) - if hasattr(file, "isatty") and file.isatty() and colorama: - file = colorama.AnsiToWin32(file).stream - self.encoding = encoding or getattr(file, 'encoding', "utf-8") - self._file = file - self.hasmarkup = should_do_markup(file) - self._lastlen = 0 - self._chars_on_current_line = 0 - self._width_of_current_line = 0 - - @property - def fullwidth(self): - if hasattr(self, '_terminal_width'): - return self._terminal_width - return get_terminal_width() - - @fullwidth.setter - def fullwidth(self, value): - self._terminal_width = value - - @property - def chars_on_current_line(self): - """Return the number of characters written so far in the current line. - - Please note that this count does not produce correct results after a reline() call, - see #164. - - .. versionadded:: 1.5.0 - - :rtype: int - """ - return self._chars_on_current_line - - @property - def width_of_current_line(self): - """Return an estimate of the width so far in the current line. - - .. versionadded:: 1.6.0 - - :rtype: int - """ - return self._width_of_current_line - - def _escaped(self, text, esc): - if esc and self.hasmarkup: - text = (''.join(['\x1b[%sm' % cod for cod in esc]) + - text +'\x1b[0m') - return text - - def markup(self, text, **kw): - esc = [] - for name in kw: - if name not in self._esctable: - raise ValueError("unknown markup: %r" %(name,)) - if kw[name]: - esc.append(self._esctable[name]) - return self._escaped(text, tuple(esc)) - - def sep(self, sepchar, title=None, fullwidth=None, **kw): - if fullwidth is None: - fullwidth = self.fullwidth - # the goal is to have the line be as long as possible - # under the condition that len(line) <= fullwidth - if sys.platform == "win32": - # if we print in the last column on windows we are on a - # new line but there is no way to verify/neutralize this - # (we may not know the exact line width) - # so let's be defensive to avoid empty lines in the output - fullwidth -= 1 - if title is not None: - # we want 2 + 2*len(fill) + len(title) <= fullwidth - # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth - # 2*len(sepchar)*N <= fullwidth - len(title) - 2 - # N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) - N = max((fullwidth - len(title) - 2) // (2*len(sepchar)), 1) - fill = sepchar * N - line = "%s %s %s" % (fill, title, fill) - else: - # we want len(sepchar)*N <= fullwidth - # i.e. N <= fullwidth // len(sepchar) - line = sepchar * (fullwidth // len(sepchar)) - # in some situations there is room for an extra sepchar at the right, - # in particular if we consider that with a sepchar like "_ " the - # trailing space is not important at the end of the line - if len(line) + len(sepchar.rstrip()) <= fullwidth: - line += sepchar.rstrip() - - self.line(line, **kw) - - def write(self, msg, **kw): - if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) - - self._update_chars_on_current_line(msg) - - if self.hasmarkup and kw: - markupmsg = self.markup(msg, **kw) - else: - markupmsg = msg - write_out(self._file, markupmsg) - - def _update_chars_on_current_line(self, text_or_bytes): - newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' - current_line = text_or_bytes.rsplit(newline, 1)[-1] - if isinstance(current_line, bytes): - current_line = current_line.decode('utf-8', errors='replace') - if newline in text_or_bytes: - self._chars_on_current_line = len(current_line) - self._width_of_current_line = get_line_width(current_line) - else: - self._chars_on_current_line += len(current_line) - self._width_of_current_line += get_line_width(current_line) - - def line(self, s='', **kw): - self.write(s, **kw) - self._checkfill(s) - self.write('\n') - - def reline(self, line, **kw): - if not self.hasmarkup: - raise ValueError("cannot use rewrite-line without terminal") - self.write(line, **kw) - self._checkfill(line) - self.write('\r') - self._lastlen = len(line) - - def _checkfill(self, line): - diff2last = self._lastlen - len(line) - if diff2last > 0: - self.write(" " * diff2last) - -class Win32ConsoleWriter(TerminalWriter): - def write(self, msg, **kw): - if msg: - if not isinstance(msg, (bytes, text)): - msg = text(msg) - - self._update_chars_on_current_line(msg) - - oldcolors = None - if self.hasmarkup and kw: - handle = GetStdHandle(STD_OUTPUT_HANDLE) - oldcolors = GetConsoleInfo(handle).wAttributes - default_bg = oldcolors & 0x00F0 - attr = default_bg - if kw.pop('bold', False): - attr |= FOREGROUND_INTENSITY - - if kw.pop('red', False): - attr |= FOREGROUND_RED - elif kw.pop('blue', False): - attr |= FOREGROUND_BLUE - elif kw.pop('green', False): - attr |= FOREGROUND_GREEN - elif kw.pop('yellow', False): - attr |= FOREGROUND_GREEN|FOREGROUND_RED - else: - attr |= oldcolors & 0x0007 - - SetConsoleTextAttribute(handle, attr) - write_out(self._file, msg) - if oldcolors: - SetConsoleTextAttribute(handle, oldcolors) - -class WriteFile(object): - def __init__(self, writemethod, encoding=None): - self.encoding = encoding - self._writemethod = writemethod - - def write(self, data): - if self.encoding: - data = data.encode(self.encoding, "replace") - self._writemethod(data) - - def flush(self): - return - - -if win32_and_ctypes: - TerminalWriter = Win32ConsoleWriter - import ctypes - from ctypes import wintypes - - # ctypes access to the Windows console - STD_OUTPUT_HANDLE = -11 - STD_ERROR_HANDLE = -12 - FOREGROUND_BLACK = 0x0000 # black text - FOREGROUND_BLUE = 0x0001 # text color contains blue. - FOREGROUND_GREEN = 0x0002 # text color contains green. - FOREGROUND_RED = 0x0004 # text color contains red. - FOREGROUND_WHITE = 0x0007 - FOREGROUND_INTENSITY = 0x0008 # text color is intensified. - BACKGROUND_BLACK = 0x0000 # background color black - BACKGROUND_BLUE = 0x0010 # background color contains blue. - BACKGROUND_GREEN = 0x0020 # background color contains green. - BACKGROUND_RED = 0x0040 # background color contains red. - BACKGROUND_WHITE = 0x0070 - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. - - SHORT = ctypes.c_short - class COORD(ctypes.Structure): - _fields_ = [('X', SHORT), - ('Y', SHORT)] - class SMALL_RECT(ctypes.Structure): - _fields_ = [('Left', SHORT), - ('Top', SHORT), - ('Right', SHORT), - ('Bottom', SHORT)] - class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [('dwSize', COORD), - ('dwCursorPosition', COORD), - ('wAttributes', wintypes.WORD), - ('srWindow', SMALL_RECT), - ('dwMaximumWindowSize', COORD)] - - _GetStdHandle = ctypes.windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [wintypes.DWORD] - _GetStdHandle.restype = wintypes.HANDLE - def GetStdHandle(kind): - return _GetStdHandle(kind) - - SetConsoleTextAttribute = ctypes.windll.kernel32.SetConsoleTextAttribute - SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] - SetConsoleTextAttribute.restype = wintypes.BOOL - - _GetConsoleScreenBufferInfo = \ - ctypes.windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] - _GetConsoleScreenBufferInfo.restype = wintypes.BOOL - def GetConsoleInfo(handle): - info = CONSOLE_SCREEN_BUFFER_INFO() - _GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) - return info - - def _getdimensions(): - handle = GetStdHandle(STD_OUTPUT_HANDLE) - info = GetConsoleInfo(handle) - # Substract one from the width, otherwise the cursor wraps - # and the ending \n causes an empty line to display. - return info.dwSize.Y, info.dwSize.X - 1 - -def write_out(fil, msg): - # XXX sometimes "msg" is of type bytes, sometimes text which - # complicates the situation. Should we try to enforce unicode? - try: - # on py27 and above writing out to sys.stdout with an encoding - # should usually work for unicode messages (if the encoding is - # capable of it) - fil.write(msg) - except UnicodeEncodeError: - # on py26 it might not work because stdout expects bytes - if fil.encoding: - try: - fil.write(msg.encode(fil.encoding)) - except UnicodeEncodeError: - # it might still fail if the encoding is not capable - pass - else: - fil.flush() - return - # fallback: escape all unicode characters - msg = msg.encode("unicode-escape").decode("ascii") - fil.write(msg) - fil.flush() diff --git a/tests/wpt/tests/tools/third_party/py/py/_log/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_log/__init__.py deleted file mode 100644 index fad62e960d4..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_log/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" logging API ('producers' and 'consumers' connected via keywords) """ - diff --git a/tests/wpt/tests/tools/third_party/py/py/_log/log.py b/tests/wpt/tests/tools/third_party/py/py/_log/log.py deleted file mode 100644 index 56969bcb58c..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_log/log.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -basic logging functionality based on a producer/consumer scheme. - -XXX implement this API: (maybe put it into slogger.py?) - - log = Logger( - info=py.log.STDOUT, - debug=py.log.STDOUT, - command=None) - log.info("hello", "world") - log.command("hello", "world") - - log = Logger(info=Logger(something=...), - debug=py.log.STDOUT, - command=None) -""" -import py -import sys - - -class Message(object): - def __init__(self, keywords, args): - self.keywords = keywords - self.args = args - - def content(self): - return " ".join(map(str, self.args)) - - def prefix(self): - return "[%s] " % (":".join(self.keywords)) - - def __str__(self): - return self.prefix() + self.content() - - -class Producer(object): - """ (deprecated) Log producer API which sends messages to be logged - to a 'consumer' object, which then prints them to stdout, - stderr, files, etc. Used extensively by PyPy-1.1. - """ - - Message = Message # to allow later customization - keywords2consumer = {} - - def __init__(self, keywords, keywordmapper=None, **kw): - if hasattr(keywords, 'split'): - keywords = tuple(keywords.split()) - self._keywords = keywords - if keywordmapper is None: - keywordmapper = default_keywordmapper - self._keywordmapper = keywordmapper - - def __repr__(self): - return "<py.log.Producer %s>" % ":".join(self._keywords) - - def __getattr__(self, name): - if '_' in name: - raise AttributeError(name) - producer = self.__class__(self._keywords + (name,)) - setattr(self, name, producer) - return producer - - def __call__(self, *args): - """ write a message to the appropriate consumer(s) """ - func = self._keywordmapper.getconsumer(self._keywords) - if func is not None: - func(self.Message(self._keywords, args)) - -class KeywordMapper: - def __init__(self): - self.keywords2consumer = {} - - def getstate(self): - return self.keywords2consumer.copy() - - def setstate(self, state): - self.keywords2consumer.clear() - self.keywords2consumer.update(state) - - def getconsumer(self, keywords): - """ return a consumer matching the given keywords. - - tries to find the most suitable consumer by walking, starting from - the back, the list of keywords, the first consumer matching a - keyword is returned (falling back to py.log.default) - """ - for i in range(len(keywords), 0, -1): - try: - return self.keywords2consumer[keywords[:i]] - except KeyError: - continue - return self.keywords2consumer.get('default', default_consumer) - - def setconsumer(self, keywords, consumer): - """ set a consumer for a set of keywords. """ - # normalize to tuples - if isinstance(keywords, str): - keywords = tuple(filter(None, keywords.split())) - elif hasattr(keywords, '_keywords'): - keywords = keywords._keywords - elif not isinstance(keywords, tuple): - raise TypeError("key %r is not a string or tuple" % (keywords,)) - if consumer is not None and not py.builtin.callable(consumer): - if not hasattr(consumer, 'write'): - raise TypeError( - "%r should be None, callable or file-like" % (consumer,)) - consumer = File(consumer) - self.keywords2consumer[keywords] = consumer - - -def default_consumer(msg): - """ the default consumer, prints the message to stdout (using 'print') """ - sys.stderr.write(str(msg)+"\n") - -default_keywordmapper = KeywordMapper() - - -def setconsumer(keywords, consumer): - default_keywordmapper.setconsumer(keywords, consumer) - - -def setstate(state): - default_keywordmapper.setstate(state) - - -def getstate(): - return default_keywordmapper.getstate() - -# -# Consumers -# - - -class File(object): - """ log consumer wrapping a file(-like) object """ - def __init__(self, f): - assert hasattr(f, 'write') - # assert isinstance(f, file) or not hasattr(f, 'open') - self._file = f - - def __call__(self, msg): - """ write a message to the log """ - self._file.write(str(msg) + "\n") - if hasattr(self._file, 'flush'): - self._file.flush() - - -class Path(object): - """ log consumer that opens and writes to a Path """ - def __init__(self, filename, append=False, - delayed_create=False, buffering=False): - self._append = append - self._filename = str(filename) - self._buffering = buffering - if not delayed_create: - self._openfile() - - def _openfile(self): - mode = self._append and 'a' or 'w' - f = open(self._filename, mode) - self._file = f - - def __call__(self, msg): - """ write a message to the log """ - if not hasattr(self, "_file"): - self._openfile() - self._file.write(str(msg) + "\n") - if not self._buffering: - self._file.flush() - - -def STDOUT(msg): - """ consumer that writes to sys.stdout """ - sys.stdout.write(str(msg)+"\n") - - -def STDERR(msg): - """ consumer that writes to sys.stderr """ - sys.stderr.write(str(msg)+"\n") - - -class Syslog: - """ consumer that writes to the syslog daemon """ - - def __init__(self, priority=None): - if priority is None: - priority = self.LOG_INFO - self.priority = priority - - def __call__(self, msg): - """ write a message to the log """ - import syslog - syslog.syslog(self.priority, str(msg)) - - -try: - import syslog -except ImportError: - pass -else: - for _prio in "EMERG ALERT CRIT ERR WARNING NOTICE INFO DEBUG".split(): - _prio = "LOG_" + _prio - try: - setattr(Syslog, _prio, getattr(syslog, _prio)) - except AttributeError: - pass diff --git a/tests/wpt/tests/tools/third_party/py/py/_log/warning.py b/tests/wpt/tests/tools/third_party/py/py/_log/warning.py deleted file mode 100644 index 6ef20d98a2d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_log/warning.py +++ /dev/null @@ -1,79 +0,0 @@ -import py, sys - -class DeprecationWarning(DeprecationWarning): - def __init__(self, msg, path, lineno): - self.msg = msg - self.path = path - self.lineno = lineno - def __repr__(self): - return "%s:%d: %s" %(self.path, self.lineno+1, self.msg) - def __str__(self): - return self.msg - -def _apiwarn(startversion, msg, stacklevel=2, function=None): - # below is mostly COPIED from python2.4/warnings.py's def warn() - # Get context information - if isinstance(stacklevel, str): - frame = sys._getframe(1) - level = 1 - found = frame.f_code.co_filename.find(stacklevel) != -1 - while frame: - co = frame.f_code - if co.co_filename.find(stacklevel) == -1: - if found: - stacklevel = level - break - else: - found = True - level += 1 - frame = frame.f_back - else: - stacklevel = 1 - msg = "%s (since version %s)" %(msg, startversion) - warn(msg, stacklevel=stacklevel+1, function=function) - - -def warn(msg, stacklevel=1, function=None): - if function is not None: - import inspect - filename = inspect.getfile(function) - lineno = py.code.getrawcode(function).co_firstlineno - else: - try: - caller = sys._getframe(stacklevel) - except ValueError: - globals = sys.__dict__ - lineno = 1 - else: - globals = caller.f_globals - lineno = caller.f_lineno - if '__name__' in globals: - module = globals['__name__'] - else: - module = "<string>" - filename = globals.get('__file__') - if filename: - fnl = filename.lower() - if fnl.endswith(".pyc") or fnl.endswith(".pyo"): - filename = filename[:-1] - elif fnl.endswith("$py.class"): - filename = filename.replace('$py.class', '.py') - else: - if module == "__main__": - try: - filename = sys.argv[0] - except AttributeError: - # embedded interpreters don't have sys.argv, see bug #839151 - filename = '__main__' - if not filename: - filename = module - path = py.path.local(filename) - warning = DeprecationWarning(msg, path, lineno) - import warnings - warnings.warn_explicit(warning, category=Warning, - filename=str(warning.path), - lineno=warning.lineno, - registry=warnings.__dict__.setdefault( - "__warningsregistry__", {}) - ) - diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_path/__init__.py deleted file mode 100644 index 51f3246f807..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" unified file system api """ diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/cacheutil.py b/tests/wpt/tests/tools/third_party/py/py/_path/cacheutil.py deleted file mode 100644 index 99225047502..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/cacheutil.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -This module contains multithread-safe cache implementations. - -All Caches have - - getorbuild(key, builder) - delentry(key) - -methods and allow configuration when instantiating the cache class. -""" -from time import time as gettime - -class BasicCache(object): - def __init__(self, maxentries=128): - self.maxentries = maxentries - self.prunenum = int(maxentries - maxentries/8) - self._dict = {} - - def clear(self): - self._dict.clear() - - def _getentry(self, key): - return self._dict[key] - - def _putentry(self, key, entry): - self._prunelowestweight() - self._dict[key] = entry - - def delentry(self, key, raising=False): - try: - del self._dict[key] - except KeyError: - if raising: - raise - - def getorbuild(self, key, builder): - try: - entry = self._getentry(key) - except KeyError: - entry = self._build(key, builder) - self._putentry(key, entry) - return entry.value - - def _prunelowestweight(self): - """ prune out entries with lowest weight. """ - numentries = len(self._dict) - if numentries >= self.maxentries: - # evict according to entry's weight - items = [(entry.weight, key) - for key, entry in self._dict.items()] - items.sort() - index = numentries - self.prunenum - if index > 0: - for weight, key in items[:index]: - # in MT situations the element might be gone - self.delentry(key, raising=False) - -class BuildcostAccessCache(BasicCache): - """ A BuildTime/Access-counting cache implementation. - the weight of a value is computed as the product of - - num-accesses-of-a-value * time-to-build-the-value - - The values with the least such weights are evicted - if the cache maxentries threshold is superceded. - For implementation flexibility more than one object - might be evicted at a time. - """ - # time function to use for measuring build-times - - def _build(self, key, builder): - start = gettime() - val = builder() - end = gettime() - return WeightedCountingEntry(val, end-start) - - -class WeightedCountingEntry(object): - def __init__(self, value, oneweight): - self._value = value - self.weight = self._oneweight = oneweight - - def value(self): - self.weight += self._oneweight - return self._value - value = property(value) - -class AgingCache(BasicCache): - """ This cache prunes out cache entries that are too old. - """ - def __init__(self, maxentries=128, maxseconds=10.0): - super(AgingCache, self).__init__(maxentries) - self.maxseconds = maxseconds - - def _getentry(self, key): - entry = self._dict[key] - if entry.isexpired(): - self.delentry(key) - raise KeyError(key) - return entry - - def _build(self, key, builder): - val = builder() - entry = AgingEntry(val, gettime() + self.maxseconds) - return entry - -class AgingEntry(object): - def __init__(self, value, expirationtime): - self.value = value - self.weight = expirationtime - - def isexpired(self): - t = gettime() - return t >= self.weight diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/common.py b/tests/wpt/tests/tools/third_party/py/py/_path/common.py deleted file mode 100644 index 2364e5fef50..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/common.py +++ /dev/null @@ -1,459 +0,0 @@ -""" -""" -import warnings -import os -import sys -import posixpath -import fnmatch -import py - -# Moved from local.py. -iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt') - -try: - # FileNotFoundError might happen in py34, and is not available with py27. - import_errors = (ImportError, FileNotFoundError) -except NameError: - import_errors = (ImportError,) - -try: - from os import fspath -except ImportError: - def fspath(path): - """ - Return the string representation of the path. - If str or bytes is passed in, it is returned unchanged. - This code comes from PEP 519, modified to support earlier versions of - python. - - This is required for python < 3.6. - """ - if isinstance(path, (py.builtin.text, py.builtin.bytes)): - return path - - # Work from the object's type to match method resolution of other magic - # methods. - path_type = type(path) - try: - return path_type.__fspath__(path) - except AttributeError: - if hasattr(path_type, '__fspath__'): - raise - try: - import pathlib - except import_errors: - pass - else: - if isinstance(path, pathlib.PurePath): - return py.builtin.text(path) - - raise TypeError("expected str, bytes or os.PathLike object, not " - + path_type.__name__) - -class Checkers: - _depend_on_existence = 'exists', 'link', 'dir', 'file' - - def __init__(self, path): - self.path = path - - def dir(self): - raise NotImplementedError - - def file(self): - raise NotImplementedError - - def dotfile(self): - return self.path.basename.startswith('.') - - def ext(self, arg): - if not arg.startswith('.'): - arg = '.' + arg - return self.path.ext == arg - - def exists(self): - raise NotImplementedError - - def basename(self, arg): - return self.path.basename == arg - - def basestarts(self, arg): - return self.path.basename.startswith(arg) - - def relto(self, arg): - return self.path.relto(arg) - - def fnmatch(self, arg): - return self.path.fnmatch(arg) - - def endswith(self, arg): - return str(self.path).endswith(arg) - - def _evaluate(self, kw): - for name, value in kw.items(): - invert = False - meth = None - try: - meth = getattr(self, name) - except AttributeError: - if name[:3] == 'not': - invert = True - try: - meth = getattr(self, name[3:]) - except AttributeError: - pass - if meth is None: - raise TypeError( - "no %r checker available for %r" % (name, self.path)) - try: - if py.code.getrawcode(meth).co_argcount > 1: - if (not meth(value)) ^ invert: - return False - else: - if bool(value) ^ bool(meth()) ^ invert: - return False - except (py.error.ENOENT, py.error.ENOTDIR, py.error.EBUSY): - # EBUSY feels not entirely correct, - # but its kind of necessary since ENOMEDIUM - # is not accessible in python - for name in self._depend_on_existence: - if name in kw: - if kw.get(name): - return False - name = 'not' + name - if name in kw: - if not kw.get(name): - return False - return True - -class NeverRaised(Exception): - pass - -class PathBase(object): - """ shared implementation for filesystem path objects.""" - Checkers = Checkers - - def __div__(self, other): - return self.join(fspath(other)) - __truediv__ = __div__ # py3k - - def basename(self): - """ basename part of path. """ - return self._getbyspec('basename')[0] - basename = property(basename, None, None, basename.__doc__) - - def dirname(self): - """ dirname part of path. """ - return self._getbyspec('dirname')[0] - dirname = property(dirname, None, None, dirname.__doc__) - - def purebasename(self): - """ pure base name of the path.""" - return self._getbyspec('purebasename')[0] - purebasename = property(purebasename, None, None, purebasename.__doc__) - - def ext(self): - """ extension of the path (including the '.').""" - return self._getbyspec('ext')[0] - ext = property(ext, None, None, ext.__doc__) - - def dirpath(self, *args, **kwargs): - """ return the directory path joined with any given path arguments. """ - return self.new(basename='').join(*args, **kwargs) - - def read_binary(self): - """ read and return a bytestring from reading the path. """ - with self.open('rb') as f: - return f.read() - - def read_text(self, encoding): - """ read and return a Unicode string from reading the path. """ - with self.open("r", encoding=encoding) as f: - return f.read() - - - def read(self, mode='r'): - """ read and return a bytestring from reading the path. """ - with self.open(mode) as f: - return f.read() - - def readlines(self, cr=1): - """ read and return a list of lines from the path. if cr is False, the -newline will be removed from the end of each line. """ - if sys.version_info < (3, ): - mode = 'rU' - else: # python 3 deprecates mode "U" in favor of "newline" option - mode = 'r' - - if not cr: - content = self.read(mode) - return content.split('\n') - else: - f = self.open(mode) - try: - return f.readlines() - finally: - f.close() - - def load(self): - """ (deprecated) return object unpickled from self.read() """ - f = self.open('rb') - try: - import pickle - return py.error.checked_call(pickle.load, f) - finally: - f.close() - - def move(self, target): - """ move this path to target. """ - if target.relto(self): - raise py.error.EINVAL( - target, - "cannot move path into a subdirectory of itself") - try: - self.rename(target) - except py.error.EXDEV: # invalid cross-device link - self.copy(target) - self.remove() - - def __repr__(self): - """ return a string representation of this path. """ - return repr(str(self)) - - def check(self, **kw): - """ check a path for existence and properties. - - Without arguments, return True if the path exists, otherwise False. - - valid checkers:: - - file=1 # is a file - file=0 # is not a file (may not even exist) - dir=1 # is a dir - link=1 # is a link - exists=1 # exists - - You can specify multiple checker definitions, for example:: - - path.check(file=1, link=1) # a link pointing to a file - """ - if not kw: - kw = {'exists': 1} - return self.Checkers(self)._evaluate(kw) - - def fnmatch(self, pattern): - """return true if the basename/fullname matches the glob-'pattern'. - - valid pattern characters:: - - * matches everything - ? matches any single character - [seq] matches any character in seq - [!seq] matches any char not in seq - - If the pattern contains a path-separator then the full path - is used for pattern matching and a '*' is prepended to the - pattern. - - if the pattern doesn't contain a path-separator the pattern - is only matched against the basename. - """ - return FNMatcher(pattern)(self) - - def relto(self, relpath): - """ return a string which is the relative part of the path - to the given 'relpath'. - """ - if not isinstance(relpath, (str, PathBase)): - raise TypeError("%r: not a string or path object" %(relpath,)) - strrelpath = str(relpath) - if strrelpath and strrelpath[-1] != self.sep: - strrelpath += self.sep - #assert strrelpath[-1] == self.sep - #assert strrelpath[-2] != self.sep - strself = self.strpath - if sys.platform == "win32" or getattr(os, '_name', None) == 'nt': - if os.path.normcase(strself).startswith( - os.path.normcase(strrelpath)): - return strself[len(strrelpath):] - elif strself.startswith(strrelpath): - return strself[len(strrelpath):] - return "" - - def ensure_dir(self, *args): - """ ensure the path joined with args is a directory. """ - return self.ensure(*args, **{"dir": True}) - - def bestrelpath(self, dest): - """ return a string which is a relative path from self - (assumed to be a directory) to dest such that - self.join(bestrelpath) == dest and if not such - path can be determined return dest. - """ - try: - if self == dest: - return os.curdir - base = self.common(dest) - if not base: # can be the case on windows - return str(dest) - self2base = self.relto(base) - reldest = dest.relto(base) - if self2base: - n = self2base.count(self.sep) + 1 - else: - n = 0 - l = [os.pardir] * n - if reldest: - l.append(reldest) - target = dest.sep.join(l) - return target - except AttributeError: - return str(dest) - - def exists(self): - return self.check() - - def isdir(self): - return self.check(dir=1) - - def isfile(self): - return self.check(file=1) - - def parts(self, reverse=False): - """ return a root-first list of all ancestor directories - plus the path itself. - """ - current = self - l = [self] - while 1: - last = current - current = current.dirpath() - if last == current: - break - l.append(current) - if not reverse: - l.reverse() - return l - - def common(self, other): - """ return the common part shared with the other path - or None if there is no common part. - """ - last = None - for x, y in zip(self.parts(), other.parts()): - if x != y: - return last - last = x - return last - - def __add__(self, other): - """ return new path object with 'other' added to the basename""" - return self.new(basename=self.basename+str(other)) - - def __cmp__(self, other): - """ return sort value (-1, 0, +1). """ - try: - return cmp(self.strpath, other.strpath) - except AttributeError: - return cmp(str(self), str(other)) # self.path, other.path) - - def __lt__(self, other): - try: - return self.strpath < other.strpath - except AttributeError: - return str(self) < str(other) - - def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): - """ yields all paths below the current one - - fil is a filter (glob pattern or callable), if not matching the - path will not be yielded, defaulting to None (everything is - returned) - - rec is a filter (glob pattern or callable) that controls whether - a node is descended, defaulting to None - - ignore is an Exception class that is ignoredwhen calling dirlist() - on any of the paths (by default, all exceptions are reported) - - bf if True will cause a breadthfirst search instead of the - default depthfirst. Default: False - - sort if True will sort entries within each directory level. - """ - for x in Visitor(fil, rec, ignore, bf, sort).gen(self): - yield x - - def _sortlist(self, res, sort): - if sort: - if hasattr(sort, '__call__'): - warnings.warn(DeprecationWarning( - "listdir(sort=callable) is deprecated and breaks on python3" - ), stacklevel=3) - res.sort(sort) - else: - res.sort() - - def samefile(self, other): - """ return True if other refers to the same stat object as self. """ - return self.strpath == str(other) - - def __fspath__(self): - return self.strpath - -class Visitor: - def __init__(self, fil, rec, ignore, bf, sort): - if isinstance(fil, py.builtin._basestring): - fil = FNMatcher(fil) - if isinstance(rec, py.builtin._basestring): - self.rec = FNMatcher(rec) - elif not hasattr(rec, '__call__') and rec: - self.rec = lambda path: True - else: - self.rec = rec - self.fil = fil - self.ignore = ignore - self.breadthfirst = bf - self.optsort = sort and sorted or (lambda x: x) - - def gen(self, path): - try: - entries = path.listdir() - except self.ignore: - return - rec = self.rec - dirs = self.optsort([p for p in entries - if p.check(dir=1) and (rec is None or rec(p))]) - if not self.breadthfirst: - for subdir in dirs: - for p in self.gen(subdir): - yield p - for p in self.optsort(entries): - if self.fil is None or self.fil(p): - yield p - if self.breadthfirst: - for subdir in dirs: - for p in self.gen(subdir): - yield p - -class FNMatcher: - def __init__(self, pattern): - self.pattern = pattern - - def __call__(self, path): - pattern = self.pattern - - if (pattern.find(path.sep) == -1 and - iswin32 and - pattern.find(posixpath.sep) != -1): - # Running on Windows, the pattern has no Windows path separators, - # and the pattern has one or more Posix path separators. Replace - # the Posix path separators with the Windows path separator. - pattern = pattern.replace(posixpath.sep, path.sep) - - if pattern.find(path.sep) == -1: - name = path.basename - else: - name = str(path) # path.strpath # XXX svn? - if not os.path.isabs(pattern): - pattern = '*' + path.sep + pattern - return fnmatch.fnmatch(name, pattern) diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/local.py b/tests/wpt/tests/tools/third_party/py/py/_path/local.py deleted file mode 100644 index 1385a039874..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/local.py +++ /dev/null @@ -1,1030 +0,0 @@ -""" -local path implementation. -""" -from __future__ import with_statement - -from contextlib import contextmanager -import sys, os, atexit, io, uuid -import py -from py._path import common -from py._path.common import iswin32, fspath -from stat import S_ISLNK, S_ISDIR, S_ISREG - -from os.path import abspath, normpath, isabs, exists, isdir, isfile, islink, dirname - -if sys.version_info > (3,0): - def map_as_list(func, iter): - return list(map(func, iter)) -else: - map_as_list = map - -ALLOW_IMPORTLIB_MODE = sys.version_info > (3,5) -if ALLOW_IMPORTLIB_MODE: - import importlib - - -class Stat(object): - def __getattr__(self, name): - return getattr(self._osstatresult, "st_" + name) - - def __init__(self, path, osstatresult): - self.path = path - self._osstatresult = osstatresult - - @property - def owner(self): - if iswin32: - raise NotImplementedError("XXX win32") - import pwd - entry = py.error.checked_call(pwd.getpwuid, self.uid) - return entry[0] - - @property - def group(self): - """ return group name of file. """ - if iswin32: - raise NotImplementedError("XXX win32") - import grp - entry = py.error.checked_call(grp.getgrgid, self.gid) - return entry[0] - - def isdir(self): - return S_ISDIR(self._osstatresult.st_mode) - - def isfile(self): - return S_ISREG(self._osstatresult.st_mode) - - def islink(self): - st = self.path.lstat() - return S_ISLNK(self._osstatresult.st_mode) - -class PosixPath(common.PathBase): - def chown(self, user, group, rec=0): - """ change ownership to the given user and group. - user and group may be specified by a number or - by a name. if rec is True change ownership - recursively. - """ - uid = getuserid(user) - gid = getgroupid(group) - if rec: - for x in self.visit(rec=lambda x: x.check(link=0)): - if x.check(link=0): - py.error.checked_call(os.chown, str(x), uid, gid) - py.error.checked_call(os.chown, str(self), uid, gid) - - def readlink(self): - """ return value of a symbolic link. """ - return py.error.checked_call(os.readlink, self.strpath) - - def mklinkto(self, oldname): - """ posix style hard link to another name. """ - py.error.checked_call(os.link, str(oldname), str(self)) - - def mksymlinkto(self, value, absolute=1): - """ create a symbolic link with the given value (pointing to another name). """ - if absolute: - py.error.checked_call(os.symlink, str(value), self.strpath) - else: - base = self.common(value) - # with posix local paths '/' is always a common base - relsource = self.__class__(value).relto(base) - reldest = self.relto(base) - n = reldest.count(self.sep) - target = self.sep.join(('..', )*n + (relsource, )) - py.error.checked_call(os.symlink, target, self.strpath) - -def getuserid(user): - import pwd - if not isinstance(user, int): - user = pwd.getpwnam(user)[2] - return user - -def getgroupid(group): - import grp - if not isinstance(group, int): - group = grp.getgrnam(group)[2] - return group - -FSBase = not iswin32 and PosixPath or common.PathBase - -class LocalPath(FSBase): - """ object oriented interface to os.path and other local filesystem - related information. - """ - class ImportMismatchError(ImportError): - """ raised on pyimport() if there is a mismatch of __file__'s""" - - sep = os.sep - class Checkers(common.Checkers): - def _stat(self): - try: - return self._statcache - except AttributeError: - try: - self._statcache = self.path.stat() - except py.error.ELOOP: - self._statcache = self.path.lstat() - return self._statcache - - def dir(self): - return S_ISDIR(self._stat().mode) - - def file(self): - return S_ISREG(self._stat().mode) - - def exists(self): - return self._stat() - - def link(self): - st = self.path.lstat() - return S_ISLNK(st.mode) - - def __init__(self, path=None, expanduser=False): - """ Initialize and return a local Path instance. - - Path can be relative to the current directory. - If path is None it defaults to the current working directory. - If expanduser is True, tilde-expansion is performed. - Note that Path instances always carry an absolute path. - Note also that passing in a local path object will simply return - the exact same path object. Use new() to get a new copy. - """ - if path is None: - self.strpath = py.error.checked_call(os.getcwd) - else: - try: - path = fspath(path) - except TypeError: - raise ValueError("can only pass None, Path instances " - "or non-empty strings to LocalPath") - if expanduser: - path = os.path.expanduser(path) - self.strpath = abspath(path) - - def __hash__(self): - s = self.strpath - if iswin32: - s = s.lower() - return hash(s) - - def __eq__(self, other): - s1 = fspath(self) - try: - s2 = fspath(other) - except TypeError: - return False - if iswin32: - s1 = s1.lower() - try: - s2 = s2.lower() - except AttributeError: - return False - return s1 == s2 - - def __ne__(self, other): - return not (self == other) - - def __lt__(self, other): - return fspath(self) < fspath(other) - - def __gt__(self, other): - return fspath(self) > fspath(other) - - def samefile(self, other): - """ return True if 'other' references the same file as 'self'. - """ - other = fspath(other) - if not isabs(other): - other = abspath(other) - if self == other: - return True - if not hasattr(os.path, "samefile"): - return False - return py.error.checked_call( - os.path.samefile, self.strpath, other) - - def remove(self, rec=1, ignore_errors=False): - """ remove a file or directory (or a directory tree if rec=1). - if ignore_errors is True, errors while removing directories will - be ignored. - """ - if self.check(dir=1, link=0): - if rec: - # force remove of readonly files on windows - if iswin32: - self.chmod(0o700, rec=1) - import shutil - py.error.checked_call( - shutil.rmtree, self.strpath, - ignore_errors=ignore_errors) - else: - py.error.checked_call(os.rmdir, self.strpath) - else: - if iswin32: - self.chmod(0o700) - py.error.checked_call(os.remove, self.strpath) - - def computehash(self, hashtype="md5", chunksize=524288): - """ return hexdigest of hashvalue for this file. """ - try: - try: - import hashlib as mod - except ImportError: - if hashtype == "sha1": - hashtype = "sha" - mod = __import__(hashtype) - hash = getattr(mod, hashtype)() - except (AttributeError, ImportError): - raise ValueError("Don't know how to compute %r hash" %(hashtype,)) - f = self.open('rb') - try: - while 1: - buf = f.read(chunksize) - if not buf: - return hash.hexdigest() - hash.update(buf) - finally: - f.close() - - def new(self, **kw): - """ create a modified version of this path. - the following keyword arguments modify various path parts:: - - a:/some/path/to/a/file.ext - xx drive - xxxxxxxxxxxxxxxxx dirname - xxxxxxxx basename - xxxx purebasename - xxx ext - """ - obj = object.__new__(self.__class__) - if not kw: - obj.strpath = self.strpath - return obj - drive, dirname, basename, purebasename,ext = self._getbyspec( - "drive,dirname,basename,purebasename,ext") - if 'basename' in kw: - if 'purebasename' in kw or 'ext' in kw: - raise ValueError("invalid specification %r" % kw) - else: - pb = kw.setdefault('purebasename', purebasename) - try: - ext = kw['ext'] - except KeyError: - pass - else: - if ext and not ext.startswith('.'): - ext = '.' + ext - kw['basename'] = pb + ext - - if ('dirname' in kw and not kw['dirname']): - kw['dirname'] = drive - else: - kw.setdefault('dirname', dirname) - kw.setdefault('sep', self.sep) - obj.strpath = normpath( - "%(dirname)s%(sep)s%(basename)s" % kw) - return obj - - def _getbyspec(self, spec): - """ see new for what 'spec' can be. """ - res = [] - parts = self.strpath.split(self.sep) - - args = filter(None, spec.split(',') ) - append = res.append - for name in args: - if name == 'drive': - append(parts[0]) - elif name == 'dirname': - append(self.sep.join(parts[:-1])) - else: - basename = parts[-1] - if name == 'basename': - append(basename) - else: - i = basename.rfind('.') - if i == -1: - purebasename, ext = basename, '' - else: - purebasename, ext = basename[:i], basename[i:] - if name == 'purebasename': - append(purebasename) - elif name == 'ext': - append(ext) - else: - raise ValueError("invalid part specification %r" % name) - return res - - def dirpath(self, *args, **kwargs): - """ return the directory path joined with any given path arguments. """ - if not kwargs: - path = object.__new__(self.__class__) - path.strpath = dirname(self.strpath) - if args: - path = path.join(*args) - return path - return super(LocalPath, self).dirpath(*args, **kwargs) - - def join(self, *args, **kwargs): - """ return a new path by appending all 'args' as path - components. if abs=1 is used restart from root if any - of the args is an absolute path. - """ - sep = self.sep - strargs = [fspath(arg) for arg in args] - strpath = self.strpath - if kwargs.get('abs'): - newargs = [] - for arg in reversed(strargs): - if isabs(arg): - strpath = arg - strargs = newargs - break - newargs.insert(0, arg) - # special case for when we have e.g. strpath == "/" - actual_sep = "" if strpath.endswith(sep) else sep - for arg in strargs: - arg = arg.strip(sep) - if iswin32: - # allow unix style paths even on windows. - arg = arg.strip('/') - arg = arg.replace('/', sep) - strpath = strpath + actual_sep + arg - actual_sep = sep - obj = object.__new__(self.__class__) - obj.strpath = normpath(strpath) - return obj - - def open(self, mode='r', ensure=False, encoding=None): - """ return an opened file with the given mode. - - If ensure is True, create parent directories if needed. - """ - if ensure: - self.dirpath().ensure(dir=1) - if encoding: - return py.error.checked_call(io.open, self.strpath, mode, encoding=encoding) - return py.error.checked_call(open, self.strpath, mode) - - def _fastjoin(self, name): - child = object.__new__(self.__class__) - child.strpath = self.strpath + self.sep + name - return child - - def islink(self): - return islink(self.strpath) - - def check(self, **kw): - if not kw: - return exists(self.strpath) - if len(kw) == 1: - if "dir" in kw: - return not kw["dir"] ^ isdir(self.strpath) - if "file" in kw: - return not kw["file"] ^ isfile(self.strpath) - return super(LocalPath, self).check(**kw) - - _patternchars = set("*?[" + os.path.sep) - def listdir(self, fil=None, sort=None): - """ list directory contents, possibly filter by the given fil func - and possibly sorted. - """ - if fil is None and sort is None: - names = py.error.checked_call(os.listdir, self.strpath) - return map_as_list(self._fastjoin, names) - if isinstance(fil, py.builtin._basestring): - if not self._patternchars.intersection(fil): - child = self._fastjoin(fil) - if exists(child.strpath): - return [child] - return [] - fil = common.FNMatcher(fil) - names = py.error.checked_call(os.listdir, self.strpath) - res = [] - for name in names: - child = self._fastjoin(name) - if fil is None or fil(child): - res.append(child) - self._sortlist(res, sort) - return res - - def size(self): - """ return size of the underlying file object """ - return self.stat().size - - def mtime(self): - """ return last modification time of the path. """ - return self.stat().mtime - - def copy(self, target, mode=False, stat=False): - """ copy path to target. - - If mode is True, will copy copy permission from path to target. - If stat is True, copy permission, last modification - time, last access time, and flags from path to target. - """ - if self.check(file=1): - if target.check(dir=1): - target = target.join(self.basename) - assert self!=target - copychunked(self, target) - if mode: - copymode(self.strpath, target.strpath) - if stat: - copystat(self, target) - else: - def rec(p): - return p.check(link=0) - for x in self.visit(rec=rec): - relpath = x.relto(self) - newx = target.join(relpath) - newx.dirpath().ensure(dir=1) - if x.check(link=1): - newx.mksymlinkto(x.readlink()) - continue - elif x.check(file=1): - copychunked(x, newx) - elif x.check(dir=1): - newx.ensure(dir=1) - if mode: - copymode(x.strpath, newx.strpath) - if stat: - copystat(x, newx) - - def rename(self, target): - """ rename this path to target. """ - target = fspath(target) - return py.error.checked_call(os.rename, self.strpath, target) - - def dump(self, obj, bin=1): - """ pickle object into path location""" - f = self.open('wb') - import pickle - try: - py.error.checked_call(pickle.dump, obj, f, bin) - finally: - f.close() - - def mkdir(self, *args): - """ create & return the directory joined with args. """ - p = self.join(*args) - py.error.checked_call(os.mkdir, fspath(p)) - return p - - def write_binary(self, data, ensure=False): - """ write binary data into path. If ensure is True create - missing parent directories. - """ - if ensure: - self.dirpath().ensure(dir=1) - with self.open('wb') as f: - f.write(data) - - def write_text(self, data, encoding, ensure=False): - """ write text data into path using the specified encoding. - If ensure is True create missing parent directories. - """ - if ensure: - self.dirpath().ensure(dir=1) - with self.open('w', encoding=encoding) as f: - f.write(data) - - def write(self, data, mode='w', ensure=False): - """ write data into path. If ensure is True create - missing parent directories. - """ - if ensure: - self.dirpath().ensure(dir=1) - if 'b' in mode: - if not py.builtin._isbytes(data): - raise ValueError("can only process bytes") - else: - if not py.builtin._istext(data): - if not py.builtin._isbytes(data): - data = str(data) - else: - data = py.builtin._totext(data, sys.getdefaultencoding()) - f = self.open(mode) - try: - f.write(data) - finally: - f.close() - - def _ensuredirs(self): - parent = self.dirpath() - if parent == self: - return self - if parent.check(dir=0): - parent._ensuredirs() - if self.check(dir=0): - try: - self.mkdir() - except py.error.EEXIST: - # race condition: file/dir created by another thread/process. - # complain if it is not a dir - if self.check(dir=0): - raise - return self - - def ensure(self, *args, **kwargs): - """ ensure that an args-joined path exists (by default as - a file). if you specify a keyword argument 'dir=True' - then the path is forced to be a directory path. - """ - p = self.join(*args) - if kwargs.get('dir', 0): - return p._ensuredirs() - else: - p.dirpath()._ensuredirs() - if not p.check(file=1): - p.open('w').close() - return p - - def stat(self, raising=True): - """ Return an os.stat() tuple. """ - if raising == True: - return Stat(self, py.error.checked_call(os.stat, self.strpath)) - try: - return Stat(self, os.stat(self.strpath)) - except KeyboardInterrupt: - raise - except Exception: - return None - - def lstat(self): - """ Return an os.lstat() tuple. """ - return Stat(self, py.error.checked_call(os.lstat, self.strpath)) - - def setmtime(self, mtime=None): - """ set modification time for the given path. if 'mtime' is None - (the default) then the file's mtime is set to current time. - - Note that the resolution for 'mtime' is platform dependent. - """ - if mtime is None: - return py.error.checked_call(os.utime, self.strpath, mtime) - try: - return py.error.checked_call(os.utime, self.strpath, (-1, mtime)) - except py.error.EINVAL: - return py.error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) - - def chdir(self): - """ change directory to self and return old current directory """ - try: - old = self.__class__() - except py.error.ENOENT: - old = None - py.error.checked_call(os.chdir, self.strpath) - return old - - - @contextmanager - def as_cwd(self): - """ - Return a context manager, which changes to the path's dir during the - managed "with" context. - On __enter__ it returns the old dir, which might be ``None``. - """ - old = self.chdir() - try: - yield old - finally: - if old is not None: - old.chdir() - - def realpath(self): - """ return a new path which contains no symbolic links.""" - return self.__class__(os.path.realpath(self.strpath)) - - def atime(self): - """ return last access time of the path. """ - return self.stat().atime - - def __repr__(self): - return 'local(%r)' % self.strpath - - def __str__(self): - """ return string representation of the Path. """ - return self.strpath - - def chmod(self, mode, rec=0): - """ change permissions to the given mode. If mode is an - integer it directly encodes the os-specific modes. - if rec is True perform recursively. - """ - if not isinstance(mode, int): - raise TypeError("mode %r must be an integer" % (mode,)) - if rec: - for x in self.visit(rec=rec): - py.error.checked_call(os.chmod, str(x), mode) - py.error.checked_call(os.chmod, self.strpath, mode) - - def pypkgpath(self): - """ return the Python package path by looking for the last - directory upwards which still contains an __init__.py. - Return None if a pkgpath can not be determined. - """ - pkgpath = None - for parent in self.parts(reverse=True): - if parent.isdir(): - if not parent.join('__init__.py').exists(): - break - if not isimportable(parent.basename): - break - pkgpath = parent - return pkgpath - - def _ensuresyspath(self, ensuremode, path): - if ensuremode: - s = str(path) - if ensuremode == "append": - if s not in sys.path: - sys.path.append(s) - else: - if s != sys.path[0]: - sys.path.insert(0, s) - - def pyimport(self, modname=None, ensuresyspath=True): - """ return path as an imported python module. - - If modname is None, look for the containing package - and construct an according module name. - The module will be put/looked up in sys.modules. - if ensuresyspath is True then the root dir for importing - the file (taking __init__.py files into account) will - be prepended to sys.path if it isn't there already. - If ensuresyspath=="append" the root dir will be appended - if it isn't already contained in sys.path. - if ensuresyspath is False no modification of syspath happens. - - Special value of ensuresyspath=="importlib" is intended - purely for using in pytest, it is capable only of importing - separate .py files outside packages, e.g. for test suite - without any __init__.py file. It effectively allows having - same-named test modules in different places and offers - mild opt-in via this option. Note that it works only in - recent versions of python. - """ - if not self.check(): - raise py.error.ENOENT(self) - - if ensuresyspath == 'importlib': - if modname is None: - modname = self.purebasename - if not ALLOW_IMPORTLIB_MODE: - raise ImportError( - "Can't use importlib due to old version of Python") - spec = importlib.util.spec_from_file_location( - modname, str(self)) - if spec is None: - raise ImportError( - "Can't find module %s at location %s" % - (modname, str(self)) - ) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - pkgpath = None - if modname is None: - pkgpath = self.pypkgpath() - if pkgpath is not None: - pkgroot = pkgpath.dirpath() - names = self.new(ext="").relto(pkgroot).split(self.sep) - if names[-1] == "__init__": - names.pop() - modname = ".".join(names) - else: - pkgroot = self.dirpath() - modname = self.purebasename - - self._ensuresyspath(ensuresyspath, pkgroot) - __import__(modname) - mod = sys.modules[modname] - if self.basename == "__init__.py": - return mod # we don't check anything as we might - # be in a namespace package ... too icky to check - modfile = mod.__file__ - if modfile[-4:] in ('.pyc', '.pyo'): - modfile = modfile[:-1] - elif modfile.endswith('$py.class'): - modfile = modfile[:-9] + '.py' - if modfile.endswith(os.path.sep + "__init__.py"): - if self.basename != "__init__.py": - modfile = modfile[:-12] - try: - issame = self.samefile(modfile) - except py.error.ENOENT: - issame = False - if not issame: - ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') - if ignore != '1': - raise self.ImportMismatchError(modname, modfile, self) - return mod - else: - try: - return sys.modules[modname] - except KeyError: - # we have a custom modname, do a pseudo-import - import types - mod = types.ModuleType(modname) - mod.__file__ = str(self) - sys.modules[modname] = mod - try: - py.builtin.execfile(str(self), mod.__dict__) - except: - del sys.modules[modname] - raise - return mod - - def sysexec(self, *argv, **popen_opts): - """ return stdout text from executing a system child process, - where the 'self' path points to executable. - The process is directly invoked and not through a system shell. - """ - from subprocess import Popen, PIPE - argv = map_as_list(str, argv) - popen_opts['stdout'] = popen_opts['stderr'] = PIPE - proc = Popen([str(self)] + argv, **popen_opts) - stdout, stderr = proc.communicate() - ret = proc.wait() - if py.builtin._isbytes(stdout): - stdout = py.builtin._totext(stdout, sys.getdefaultencoding()) - if ret != 0: - if py.builtin._isbytes(stderr): - stderr = py.builtin._totext(stderr, sys.getdefaultencoding()) - raise py.process.cmdexec.Error(ret, ret, str(self), - stdout, stderr,) - return stdout - - def sysfind(cls, name, checker=None, paths=None): - """ return a path object found by looking at the systems - underlying PATH specification. If the checker is not None - it will be invoked to filter matching paths. If a binary - cannot be found, None is returned - Note: This is probably not working on plain win32 systems - but may work on cygwin. - """ - if isabs(name): - p = py.path.local(name) - if p.check(file=1): - return p - else: - if paths is None: - if iswin32: - paths = os.environ['Path'].split(';') - if '' not in paths and '.' not in paths: - paths.append('.') - try: - systemroot = os.environ['SYSTEMROOT'] - except KeyError: - pass - else: - paths = [path.replace('%SystemRoot%', systemroot) - for path in paths] - else: - paths = os.environ['PATH'].split(':') - tryadd = [] - if iswin32: - tryadd += os.environ['PATHEXT'].split(os.pathsep) - tryadd.append("") - - for x in paths: - for addext in tryadd: - p = py.path.local(x).join(name, abs=True) + addext - try: - if p.check(file=1): - if checker: - if not checker(p): - continue - return p - except py.error.EACCES: - pass - return None - sysfind = classmethod(sysfind) - - def _gethomedir(cls): - try: - x = os.environ['HOME'] - except KeyError: - try: - x = os.environ["HOMEDRIVE"] + os.environ['HOMEPATH'] - except KeyError: - return None - return cls(x) - _gethomedir = classmethod(_gethomedir) - - # """ - # special class constructors for local filesystem paths - # """ - @classmethod - def get_temproot(cls): - """ return the system's temporary directory - (where tempfiles are usually created in) - """ - import tempfile - return py.path.local(tempfile.gettempdir()) - - @classmethod - def mkdtemp(cls, rootdir=None): - """ return a Path object pointing to a fresh new temporary directory - (which we created ourself). - """ - import tempfile - if rootdir is None: - rootdir = cls.get_temproot() - return cls(py.error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) - - def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, - lock_timeout=172800): # two days - """ return unique directory with a number greater than the current - maximum one. The number is assumed to start directly after prefix. - if keep is true directories with a number less than (maxnum-keep) - will be removed. If .lock files are used (lock_timeout non-zero), - algorithm is multi-process safe. - """ - if rootdir is None: - rootdir = cls.get_temproot() - - nprefix = prefix.lower() - def parse_num(path): - """ parse the number out of a path (if it matches the prefix) """ - nbasename = path.basename.lower() - if nbasename.startswith(nprefix): - try: - return int(nbasename[len(nprefix):]) - except ValueError: - pass - - def create_lockfile(path): - """ exclusively create lockfile. Throws when failed """ - mypid = os.getpid() - lockfile = path.join('.lock') - if hasattr(lockfile, 'mksymlinkto'): - lockfile.mksymlinkto(str(mypid)) - else: - fd = py.error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - with os.fdopen(fd, 'w') as f: - f.write(str(mypid)) - return lockfile - - def atexit_remove_lockfile(lockfile): - """ ensure lockfile is removed at process exit """ - mypid = os.getpid() - def try_remove_lockfile(): - # in a fork() situation, only the last process should - # remove the .lock, otherwise the other processes run the - # risk of seeing their temporary dir disappear. For now - # we remove the .lock in the parent only (i.e. we assume - # that the children finish before the parent). - if os.getpid() != mypid: - return - try: - lockfile.remove() - except py.error.Error: - pass - atexit.register(try_remove_lockfile) - - # compute the maximum number currently in use with the prefix - lastmax = None - while True: - maxnum = -1 - for path in rootdir.listdir(): - num = parse_num(path) - if num is not None: - maxnum = max(maxnum, num) - - # make the new directory - try: - udir = rootdir.mkdir(prefix + str(maxnum+1)) - if lock_timeout: - lockfile = create_lockfile(udir) - atexit_remove_lockfile(lockfile) - except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): - # race condition (1): another thread/process created the dir - # in the meantime - try again - # race condition (2): another thread/process spuriously acquired - # lock treating empty directory as candidate - # for removal - try again - # race condition (3): another thread/process tried to create the lock at - # the same time (happened in Python 3.3 on Windows) - # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa - if lastmax == maxnum: - raise - lastmax = maxnum - continue - break - - def get_mtime(path): - """ read file modification time """ - try: - return path.lstat().mtime - except py.error.Error: - pass - - garbage_prefix = prefix + 'garbage-' - - def is_garbage(path): - """ check if path denotes directory scheduled for removal """ - bn = path.basename - return bn.startswith(garbage_prefix) - - # prune old directories - udir_time = get_mtime(udir) - if keep and udir_time: - for path in rootdir.listdir(): - num = parse_num(path) - if num is not None and num <= (maxnum - keep): - try: - # try acquiring lock to remove directory as exclusive user - if lock_timeout: - create_lockfile(path) - except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): - path_time = get_mtime(path) - if not path_time: - # assume directory doesn't exist now - continue - if abs(udir_time - path_time) < lock_timeout: - # assume directory with lockfile exists - # and lock timeout hasn't expired yet - continue - - # path dir locked for exclusive use - # and scheduled for removal to avoid another thread/process - # treating it as a new directory or removal candidate - garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) - try: - path.rename(garbage_path) - garbage_path.remove(rec=1) - except KeyboardInterrupt: - raise - except: # this might be py.error.Error, WindowsError ... - pass - if is_garbage(path): - try: - path.remove(rec=1) - except KeyboardInterrupt: - raise - except: # this might be py.error.Error, WindowsError ... - pass - - # make link... - try: - username = os.environ['USER'] #linux, et al - except KeyError: - try: - username = os.environ['USERNAME'] #windows - except KeyError: - username = 'current' - - src = str(udir) - dest = src[:src.rfind('-')] + '-' + username - try: - os.unlink(dest) - except OSError: - pass - try: - os.symlink(src, dest) - except (OSError, AttributeError, NotImplementedError): - pass - - return udir - make_numbered_dir = classmethod(make_numbered_dir) - - -def copymode(src, dest): - """ copy permission from src to dst. """ - import shutil - shutil.copymode(src, dest) - - -def copystat(src, dest): - """ copy permission, last modification time, - last access time, and flags from src to dst.""" - import shutil - shutil.copystat(str(src), str(dest)) - - -def copychunked(src, dest): - chunksize = 524288 # half a meg of bytes - fsrc = src.open('rb') - try: - fdest = dest.open('wb') - try: - while 1: - buf = fsrc.read(chunksize) - if not buf: - break - fdest.write(buf) - finally: - fdest.close() - finally: - fsrc.close() - - -def isimportable(name): - if name and (name[0].isalpha() or name[0] == '_'): - name = name.replace("_", '') - return not name or name.isalnum() diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/svnurl.py b/tests/wpt/tests/tools/third_party/py/py/_path/svnurl.py deleted file mode 100644 index 6589a71d09e..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/svnurl.py +++ /dev/null @@ -1,380 +0,0 @@ -""" -module defining a subversion path object based on the external -command 'svn'. This modules aims to work with svn 1.3 and higher -but might also interact well with earlier versions. -""" - -import os, sys, time, re -import py -from py import path, process -from py._path import common -from py._path import svnwc as svncommon -from py._path.cacheutil import BuildcostAccessCache, AgingCache - -DEBUG=False - -class SvnCommandPath(svncommon.SvnPathBase): - """ path implementation that offers access to (possibly remote) subversion - repositories. """ - - _lsrevcache = BuildcostAccessCache(maxentries=128) - _lsnorevcache = AgingCache(maxentries=1000, maxseconds=60.0) - - def __new__(cls, path, rev=None, auth=None): - self = object.__new__(cls) - if isinstance(path, cls): - rev = path.rev - auth = path.auth - path = path.strpath - svncommon.checkbadchars(path) - path = path.rstrip('/') - self.strpath = path - self.rev = rev - self.auth = auth - return self - - def __repr__(self): - if self.rev == -1: - return 'svnurl(%r)' % self.strpath - else: - return 'svnurl(%r, %r)' % (self.strpath, self.rev) - - def _svnwithrev(self, cmd, *args): - """ execute an svn command, append our own url and revision """ - if self.rev is None: - return self._svnwrite(cmd, *args) - else: - args = ['-r', self.rev] + list(args) - return self._svnwrite(cmd, *args) - - def _svnwrite(self, cmd, *args): - """ execute an svn command, append our own url """ - l = ['svn %s' % cmd] - args = ['"%s"' % self._escape(item) for item in args] - l.extend(args) - l.append('"%s"' % self._encodedurl()) - # fixing the locale because we can't otherwise parse - string = " ".join(l) - if DEBUG: - print("execing %s" % string) - out = self._svncmdexecauth(string) - return out - - def _svncmdexecauth(self, cmd): - """ execute an svn command 'as is' """ - cmd = svncommon.fixlocale() + cmd - if self.auth is not None: - cmd += ' ' + self.auth.makecmdoptions() - return self._cmdexec(cmd) - - def _cmdexec(self, cmd): - try: - out = process.cmdexec(cmd) - except py.process.cmdexec.Error: - e = sys.exc_info()[1] - if (e.err.find('File Exists') != -1 or - e.err.find('File already exists') != -1): - raise py.error.EEXIST(self) - raise - return out - - def _svnpopenauth(self, cmd): - """ execute an svn command, return a pipe for reading stdin """ - cmd = svncommon.fixlocale() + cmd - if self.auth is not None: - cmd += ' ' + self.auth.makecmdoptions() - return self._popen(cmd) - - def _popen(self, cmd): - return os.popen(cmd) - - def _encodedurl(self): - return self._escape(self.strpath) - - def _norev_delentry(self, path): - auth = self.auth and self.auth.makecmdoptions() or None - self._lsnorevcache.delentry((str(path), auth)) - - def open(self, mode='r'): - """ return an opened file with the given mode. """ - if mode not in ("r", "rU",): - raise ValueError("mode %r not supported" % (mode,)) - assert self.check(file=1) # svn cat returns an empty file otherwise - if self.rev is None: - return self._svnpopenauth('svn cat "%s"' % ( - self._escape(self.strpath), )) - else: - return self._svnpopenauth('svn cat -r %s "%s"' % ( - self.rev, self._escape(self.strpath))) - - def dirpath(self, *args, **kwargs): - """ return the directory path of the current path joined - with any given path arguments. - """ - l = self.strpath.split(self.sep) - if len(l) < 4: - raise py.error.EINVAL(self, "base is not valid") - elif len(l) == 4: - return self.join(*args, **kwargs) - else: - return self.new(basename='').join(*args, **kwargs) - - # modifying methods (cache must be invalidated) - def mkdir(self, *args, **kwargs): - """ create & return the directory joined with args. - pass a 'msg' keyword argument to set the commit message. - """ - commit_msg = kwargs.get('msg', "mkdir by py lib invocation") - createpath = self.join(*args) - createpath._svnwrite('mkdir', '-m', commit_msg) - self._norev_delentry(createpath.dirpath()) - return createpath - - def copy(self, target, msg='copied by py lib invocation'): - """ copy path to target with checkin message msg.""" - if getattr(target, 'rev', None) is not None: - raise py.error.EINVAL(target, "revisions are immutable") - self._svncmdexecauth('svn copy -m "%s" "%s" "%s"' %(msg, - self._escape(self), self._escape(target))) - self._norev_delentry(target.dirpath()) - - def rename(self, target, msg="renamed by py lib invocation"): - """ rename this path to target with checkin message msg. """ - if getattr(self, 'rev', None) is not None: - raise py.error.EINVAL(self, "revisions are immutable") - self._svncmdexecauth('svn move -m "%s" --force "%s" "%s"' %( - msg, self._escape(self), self._escape(target))) - self._norev_delentry(self.dirpath()) - self._norev_delentry(self) - - def remove(self, rec=1, msg='removed by py lib invocation'): - """ remove a file or directory (or a directory tree if rec=1) with -checkin message msg.""" - if self.rev is not None: - raise py.error.EINVAL(self, "revisions are immutable") - self._svncmdexecauth('svn rm -m "%s" "%s"' %(msg, self._escape(self))) - self._norev_delentry(self.dirpath()) - - def export(self, topath): - """ export to a local path - - topath should not exist prior to calling this, returns a - py.path.local instance - """ - topath = py.path.local(topath) - args = ['"%s"' % (self._escape(self),), - '"%s"' % (self._escape(topath),)] - if self.rev is not None: - args = ['-r', str(self.rev)] + args - self._svncmdexecauth('svn export %s' % (' '.join(args),)) - return topath - - def ensure(self, *args, **kwargs): - """ ensure that an args-joined path exists (by default as - a file). If you specify a keyword argument 'dir=True' - then the path is forced to be a directory path. - """ - if getattr(self, 'rev', None) is not None: - raise py.error.EINVAL(self, "revisions are immutable") - target = self.join(*args) - dir = kwargs.get('dir', 0) - for x in target.parts(reverse=True): - if x.check(): - break - else: - raise py.error.ENOENT(target, "has not any valid base!") - if x == target: - if not x.check(dir=dir): - raise dir and py.error.ENOTDIR(x) or py.error.EISDIR(x) - return x - tocreate = target.relto(x) - basename = tocreate.split(self.sep, 1)[0] - tempdir = py.path.local.mkdtemp() - try: - tempdir.ensure(tocreate, dir=dir) - cmd = 'svn import -m "%s" "%s" "%s"' % ( - "ensure %s" % self._escape(tocreate), - self._escape(tempdir.join(basename)), - x.join(basename)._encodedurl()) - self._svncmdexecauth(cmd) - self._norev_delentry(x) - finally: - tempdir.remove() - return target - - # end of modifying methods - def _propget(self, name): - res = self._svnwithrev('propget', name) - return res[:-1] # strip trailing newline - - def _proplist(self): - res = self._svnwithrev('proplist') - lines = res.split('\n') - lines = [x.strip() for x in lines[1:]] - return svncommon.PropListDict(self, lines) - - def info(self): - """ return an Info structure with svn-provided information. """ - parent = self.dirpath() - nameinfo_seq = parent._listdir_nameinfo() - bn = self.basename - for name, info in nameinfo_seq: - if name == bn: - return info - raise py.error.ENOENT(self) - - - def _listdir_nameinfo(self): - """ return sequence of name-info directory entries of self """ - def builder(): - try: - res = self._svnwithrev('ls', '-v') - except process.cmdexec.Error: - e = sys.exc_info()[1] - if e.err.find('non-existent in that revision') != -1: - raise py.error.ENOENT(self, e.err) - elif e.err.find("E200009:") != -1: - raise py.error.ENOENT(self, e.err) - elif e.err.find('File not found') != -1: - raise py.error.ENOENT(self, e.err) - elif e.err.find('not part of a repository')!=-1: - raise py.error.ENOENT(self, e.err) - elif e.err.find('Unable to open')!=-1: - raise py.error.ENOENT(self, e.err) - elif e.err.lower().find('method not allowed')!=-1: - raise py.error.EACCES(self, e.err) - raise py.error.Error(e.err) - lines = res.split('\n') - nameinfo_seq = [] - for lsline in lines: - if lsline: - info = InfoSvnCommand(lsline) - if info._name != '.': # svn 1.5 produces '.' dirs, - nameinfo_seq.append((info._name, info)) - nameinfo_seq.sort() - return nameinfo_seq - auth = self.auth and self.auth.makecmdoptions() or None - if self.rev is not None: - return self._lsrevcache.getorbuild((self.strpath, self.rev, auth), - builder) - else: - return self._lsnorevcache.getorbuild((self.strpath, auth), - builder) - - def listdir(self, fil=None, sort=None): - """ list directory contents, possibly filter by the given fil func - and possibly sorted. - """ - if isinstance(fil, str): - fil = common.FNMatcher(fil) - nameinfo_seq = self._listdir_nameinfo() - if len(nameinfo_seq) == 1: - name, info = nameinfo_seq[0] - if name == self.basename and info.kind == 'file': - #if not self.check(dir=1): - raise py.error.ENOTDIR(self) - paths = [self.join(name) for (name, info) in nameinfo_seq] - if fil: - paths = [x for x in paths if fil(x)] - self._sortlist(paths, sort) - return paths - - - def log(self, rev_start=None, rev_end=1, verbose=False): - """ return a list of LogEntry instances for this path. -rev_start is the starting revision (defaulting to the first one). -rev_end is the last revision (defaulting to HEAD). -if verbose is True, then the LogEntry instances also know which files changed. -""" - assert self.check() #make it simpler for the pipe - rev_start = rev_start is None and "HEAD" or rev_start - rev_end = rev_end is None and "HEAD" or rev_end - - if rev_start == "HEAD" and rev_end == 1: - rev_opt = "" - else: - rev_opt = "-r %s:%s" % (rev_start, rev_end) - verbose_opt = verbose and "-v" or "" - xmlpipe = self._svnpopenauth('svn log --xml %s %s "%s"' % - (rev_opt, verbose_opt, self.strpath)) - from xml.dom import minidom - tree = minidom.parse(xmlpipe) - result = [] - for logentry in filter(None, tree.firstChild.childNodes): - if logentry.nodeType == logentry.ELEMENT_NODE: - result.append(svncommon.LogEntry(logentry)) - return result - -#01234567890123456789012345678901234567890123467 -# 2256 hpk 165 Nov 24 17:55 __init__.py -# XXX spotted by Guido, SVN 1.3.0 has different aligning, breaks the code!!! -# 1312 johnny 1627 May 05 14:32 test_decorators.py -# -class InfoSvnCommand: - # the '0?' part in the middle is an indication of whether the resource is - # locked, see 'svn help ls' - lspattern = re.compile( - r'^ *(?P<rev>\d+) +(?P<author>.+?) +(0? *(?P<size>\d+))? ' - r'*(?P<date>\w+ +\d{2} +[\d:]+) +(?P<file>.*)$') - def __init__(self, line): - # this is a typical line from 'svn ls http://...' - #_ 1127 jum 0 Jul 13 15:28 branch/ - match = self.lspattern.match(line) - data = match.groupdict() - self._name = data['file'] - if self._name[-1] == '/': - self._name = self._name[:-1] - self.kind = 'dir' - else: - self.kind = 'file' - #self.has_props = l.pop(0) == 'P' - self.created_rev = int(data['rev']) - self.last_author = data['author'] - self.size = data['size'] and int(data['size']) or 0 - self.mtime = parse_time_with_missing_year(data['date']) - self.time = self.mtime * 1000000 - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - -#____________________________________________________ -# -# helper functions -#____________________________________________________ -def parse_time_with_missing_year(timestr): - """ analyze the time part from a single line of "svn ls -v" - the svn output doesn't show the year makes the 'timestr' - ambigous. - """ - import calendar - t_now = time.gmtime() - - tparts = timestr.split() - month = time.strptime(tparts.pop(0), '%b')[1] - day = time.strptime(tparts.pop(0), '%d')[2] - last = tparts.pop(0) # year or hour:minute - try: - if ":" in last: - raise ValueError() - year = time.strptime(last, '%Y')[0] - hour = minute = 0 - except ValueError: - hour, minute = time.strptime(last, '%H:%M')[3:5] - year = t_now[0] - - t_result = (year, month, day, hour, minute, 0,0,0,0) - if t_result > t_now: - year -= 1 - t_result = (year, month, day, hour, minute, 0,0,0,0) - return calendar.timegm(t_result) - -class PathEntry: - def __init__(self, ppart): - self.strpath = ppart.firstChild.nodeValue.encode('UTF-8') - self.action = ppart.getAttribute('action').encode('UTF-8') - if self.action == 'A': - self.copyfrom_path = ppart.getAttribute('copyfrom-path').encode('UTF-8') - if self.copyfrom_path: - self.copyfrom_rev = int(ppart.getAttribute('copyfrom-rev')) - diff --git a/tests/wpt/tests/tools/third_party/py/py/_path/svnwc.py b/tests/wpt/tests/tools/third_party/py/py/_path/svnwc.py deleted file mode 100644 index b5b9d8d544a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_path/svnwc.py +++ /dev/null @@ -1,1240 +0,0 @@ -""" -svn-Command based Implementation of a Subversion WorkingCopy Path. - - SvnWCCommandPath is the main class. - -""" - -import os, sys, time, re, calendar -import py -import subprocess -from py._path import common - -#----------------------------------------------------------- -# Caching latest repository revision and repo-paths -# (getting them is slow with the current implementations) -# -# XXX make mt-safe -#----------------------------------------------------------- - -class cache: - proplist = {} - info = {} - entries = {} - prop = {} - -class RepoEntry: - def __init__(self, url, rev, timestamp): - self.url = url - self.rev = rev - self.timestamp = timestamp - - def __str__(self): - return "repo: %s;%s %s" %(self.url, self.rev, self.timestamp) - -class RepoCache: - """ The Repocache manages discovered repository paths - and their revisions. If inside a timeout the cache - will even return the revision of the root. - """ - timeout = 20 # seconds after which we forget that we know the last revision - - def __init__(self): - self.repos = [] - - def clear(self): - self.repos = [] - - def put(self, url, rev, timestamp=None): - if rev is None: - return - if timestamp is None: - timestamp = time.time() - - for entry in self.repos: - if url == entry.url: - entry.timestamp = timestamp - entry.rev = rev - #print "set repo", entry - break - else: - entry = RepoEntry(url, rev, timestamp) - self.repos.append(entry) - #print "appended repo", entry - - def get(self, url): - now = time.time() - for entry in self.repos: - if url.startswith(entry.url): - if now < entry.timestamp + self.timeout: - #print "returning immediate Etrny", entry - return entry.url, entry.rev - return entry.url, -1 - return url, -1 - -repositories = RepoCache() - - -# svn support code - -ALLOWED_CHARS = "_ -/\\=$.~+%" #add characters as necessary when tested -if sys.platform == "win32": - ALLOWED_CHARS += ":" -ALLOWED_CHARS_HOST = ALLOWED_CHARS + '@:' - -def _getsvnversion(ver=[]): - try: - return ver[0] - except IndexError: - v = py.process.cmdexec("svn -q --version") - v.strip() - v = '.'.join(v.split('.')[:2]) - ver.append(v) - return v - -def _escape_helper(text): - text = str(text) - if sys.platform != 'win32': - text = str(text).replace('$', '\\$') - return text - -def _check_for_bad_chars(text, allowed_chars=ALLOWED_CHARS): - for c in str(text): - if c.isalnum(): - continue - if c in allowed_chars: - continue - return True - return False - -def checkbadchars(url): - # (hpk) not quite sure about the exact purpose, guido w.? - proto, uri = url.split("://", 1) - if proto != "file": - host, uripath = uri.split('/', 1) - # only check for bad chars in the non-protocol parts - if (_check_for_bad_chars(host, ALLOWED_CHARS_HOST) \ - or _check_for_bad_chars(uripath, ALLOWED_CHARS)): - raise ValueError("bad char in %r" % (url, )) - - -#_______________________________________________________________ - -class SvnPathBase(common.PathBase): - """ Base implementation for SvnPath implementations. """ - sep = '/' - - def _geturl(self): - return self.strpath - url = property(_geturl, None, None, "url of this svn-path.") - - def __str__(self): - """ return a string representation (including rev-number) """ - return self.strpath - - def __hash__(self): - return hash(self.strpath) - - def new(self, **kw): - """ create a modified version of this path. A 'rev' argument - indicates a new revision. - the following keyword arguments modify various path parts:: - - http://host.com/repo/path/file.ext - |-----------------------| dirname - |------| basename - |--| purebasename - |--| ext - """ - obj = object.__new__(self.__class__) - obj.rev = kw.get('rev', self.rev) - obj.auth = kw.get('auth', self.auth) - dirname, basename, purebasename, ext = self._getbyspec( - "dirname,basename,purebasename,ext") - if 'basename' in kw: - if 'purebasename' in kw or 'ext' in kw: - raise ValueError("invalid specification %r" % kw) - else: - pb = kw.setdefault('purebasename', purebasename) - ext = kw.setdefault('ext', ext) - if ext and not ext.startswith('.'): - ext = '.' + ext - kw['basename'] = pb + ext - - kw.setdefault('dirname', dirname) - kw.setdefault('sep', self.sep) - if kw['basename']: - obj.strpath = "%(dirname)s%(sep)s%(basename)s" % kw - else: - obj.strpath = "%(dirname)s" % kw - return obj - - def _getbyspec(self, spec): - """ get specified parts of the path. 'arg' is a string - with comma separated path parts. The parts are returned - in exactly the order of the specification. - - you may specify the following parts: - - http://host.com/repo/path/file.ext - |-----------------------| dirname - |------| basename - |--| purebasename - |--| ext - """ - res = [] - parts = self.strpath.split(self.sep) - for name in spec.split(','): - name = name.strip() - if name == 'dirname': - res.append(self.sep.join(parts[:-1])) - elif name == 'basename': - res.append(parts[-1]) - else: - basename = parts[-1] - i = basename.rfind('.') - if i == -1: - purebasename, ext = basename, '' - else: - purebasename, ext = basename[:i], basename[i:] - if name == 'purebasename': - res.append(purebasename) - elif name == 'ext': - res.append(ext) - else: - raise NameError("Don't know part %r" % name) - return res - - def __eq__(self, other): - """ return true if path and rev attributes each match """ - return (str(self) == str(other) and - (self.rev == other.rev or self.rev == other.rev)) - - def __ne__(self, other): - return not self == other - - def join(self, *args): - """ return a new Path (with the same revision) which is composed - of the self Path followed by 'args' path components. - """ - if not args: - return self - - args = tuple([arg.strip(self.sep) for arg in args]) - parts = (self.strpath, ) + args - newpath = self.__class__(self.sep.join(parts), self.rev, self.auth) - return newpath - - def propget(self, name): - """ return the content of the given property. """ - value = self._propget(name) - return value - - def proplist(self): - """ list all property names. """ - content = self._proplist() - return content - - def size(self): - """ Return the size of the file content of the Path. """ - return self.info().size - - def mtime(self): - """ Return the last modification time of the file. """ - return self.info().mtime - - # shared help methods - - def _escape(self, cmd): - return _escape_helper(cmd) - - - #def _childmaxrev(self): - # """ return maximum revision number of childs (or self.rev if no childs) """ - # rev = self.rev - # for name, info in self._listdir_nameinfo(): - # rev = max(rev, info.created_rev) - # return rev - - #def _getlatestrevision(self): - # """ return latest repo-revision for this path. """ - # url = self.strpath - # path = self.__class__(url, None) - # - # # we need a long walk to find the root-repo and revision - # while 1: - # try: - # rev = max(rev, path._childmaxrev()) - # previous = path - # path = path.dirpath() - # except (IOError, process.cmdexec.Error): - # break - # if rev is None: - # raise IOError, "could not determine newest repo revision for %s" % self - # return rev - - class Checkers(common.Checkers): - def dir(self): - try: - return self.path.info().kind == 'dir' - except py.error.Error: - return self._listdirworks() - - def _listdirworks(self): - try: - self.path.listdir() - except py.error.ENOENT: - return False - else: - return True - - def file(self): - try: - return self.path.info().kind == 'file' - except py.error.ENOENT: - return False - - def exists(self): - try: - return self.path.info() - except py.error.ENOENT: - return self._listdirworks() - -def parse_apr_time(timestr): - i = timestr.rfind('.') - if i == -1: - raise ValueError("could not parse %s" % timestr) - timestr = timestr[:i] - parsedtime = time.strptime(timestr, "%Y-%m-%dT%H:%M:%S") - return time.mktime(parsedtime) - -class PropListDict(dict): - """ a Dictionary which fetches values (InfoSvnCommand instances) lazily""" - def __init__(self, path, keynames): - dict.__init__(self, [(x, None) for x in keynames]) - self.path = path - - def __getitem__(self, key): - value = dict.__getitem__(self, key) - if value is None: - value = self.path.propget(key) - dict.__setitem__(self, key, value) - return value - -def fixlocale(): - if sys.platform != 'win32': - return 'LC_ALL=C ' - return '' - -# some nasty chunk of code to solve path and url conversion and quoting issues -ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ') -if os.sep in ILLEGAL_CHARS: - ILLEGAL_CHARS.remove(os.sep) -ISWINDOWS = sys.platform == 'win32' -_reg_allow_disk = re.compile(r'^([a-z]\:\\)?[^:]+$', re.I) -def _check_path(path): - illegal = ILLEGAL_CHARS[:] - sp = path.strpath - if ISWINDOWS: - illegal.remove(':') - if not _reg_allow_disk.match(sp): - raise ValueError('path may not contain a colon (:)') - for char in sp: - if char not in string.printable or char in illegal: - raise ValueError('illegal character %r in path' % (char,)) - -def path_to_fspath(path, addat=True): - _check_path(path) - sp = path.strpath - if addat and path.rev != -1: - sp = '%s@%s' % (sp, path.rev) - elif addat: - sp = '%s@HEAD' % (sp,) - return sp - -def url_from_path(path): - fspath = path_to_fspath(path, False) - from urllib import quote - if ISWINDOWS: - match = _reg_allow_disk.match(fspath) - fspath = fspath.replace('\\', '/') - if match.group(1): - fspath = '/%s%s' % (match.group(1).replace('\\', '/'), - quote(fspath[len(match.group(1)):])) - else: - fspath = quote(fspath) - else: - fspath = quote(fspath) - if path.rev != -1: - fspath = '%s@%s' % (fspath, path.rev) - else: - fspath = '%s@HEAD' % (fspath,) - return 'file://%s' % (fspath,) - -class SvnAuth(object): - """ container for auth information for Subversion """ - def __init__(self, username, password, cache_auth=True, interactive=True): - self.username = username - self.password = password - self.cache_auth = cache_auth - self.interactive = interactive - - def makecmdoptions(self): - uname = self.username.replace('"', '\\"') - passwd = self.password.replace('"', '\\"') - ret = [] - if uname: - ret.append('--username="%s"' % (uname,)) - if passwd: - ret.append('--password="%s"' % (passwd,)) - if not self.cache_auth: - ret.append('--no-auth-cache') - if not self.interactive: - ret.append('--non-interactive') - return ' '.join(ret) - - def __str__(self): - return "<SvnAuth username=%s ...>" %(self.username,) - -rex_blame = re.compile(r'\s*(\d+)\s+(\S+) (.*)') - -class SvnWCCommandPath(common.PathBase): - """ path implementation offering access/modification to svn working copies. - It has methods similar to the functions in os.path and similar to the - commands of the svn client. - """ - sep = os.sep - - def __new__(cls, wcpath=None, auth=None): - self = object.__new__(cls) - if isinstance(wcpath, cls): - if wcpath.__class__ == cls: - return wcpath - wcpath = wcpath.localpath - if _check_for_bad_chars(str(wcpath), - ALLOWED_CHARS): - raise ValueError("bad char in wcpath %s" % (wcpath, )) - self.localpath = py.path.local(wcpath) - self.auth = auth - return self - - strpath = property(lambda x: str(x.localpath), None, None, "string path") - rev = property(lambda x: x.info(usecache=0).rev, None, None, "revision") - - def __eq__(self, other): - return self.localpath == getattr(other, 'localpath', None) - - def _geturl(self): - if getattr(self, '_url', None) is None: - info = self.info() - self._url = info.url #SvnPath(info.url, info.rev) - assert isinstance(self._url, py.builtin._basestring) - return self._url - - url = property(_geturl, None, None, "url of this WC item") - - def _escape(self, cmd): - return _escape_helper(cmd) - - def dump(self, obj): - """ pickle object into path location""" - return self.localpath.dump(obj) - - def svnurl(self): - """ return current SvnPath for this WC-item. """ - info = self.info() - return py.path.svnurl(info.url) - - def __repr__(self): - return "svnwc(%r)" % (self.strpath) # , self._url) - - def __str__(self): - return str(self.localpath) - - def _makeauthoptions(self): - if self.auth is None: - return '' - return self.auth.makecmdoptions() - - def _authsvn(self, cmd, args=None): - args = args and list(args) or [] - args.append(self._makeauthoptions()) - return self._svn(cmd, *args) - - def _svn(self, cmd, *args): - l = ['svn %s' % cmd] - args = [self._escape(item) for item in args] - l.extend(args) - l.append('"%s"' % self._escape(self.strpath)) - # try fixing the locale because we can't otherwise parse - string = fixlocale() + " ".join(l) - try: - try: - key = 'LC_MESSAGES' - hold = os.environ.get(key) - os.environ[key] = 'C' - out = py.process.cmdexec(string) - finally: - if hold: - os.environ[key] = hold - else: - del os.environ[key] - except py.process.cmdexec.Error: - e = sys.exc_info()[1] - strerr = e.err.lower() - if strerr.find('not found') != -1: - raise py.error.ENOENT(self) - elif strerr.find("E200009:") != -1: - raise py.error.ENOENT(self) - if (strerr.find('file exists') != -1 or - strerr.find('file already exists') != -1 or - strerr.find('w150002:') != -1 or - strerr.find("can't create directory") != -1): - raise py.error.EEXIST(strerr) #self) - raise - return out - - def switch(self, url): - """ switch to given URL. """ - self._authsvn('switch', [url]) - - def checkout(self, url=None, rev=None): - """ checkout from url to local wcpath. """ - args = [] - if url is None: - url = self.url - if rev is None or rev == -1: - if (sys.platform != 'win32' and - _getsvnversion() == '1.3'): - url += "@HEAD" - else: - if _getsvnversion() == '1.3': - url += "@%d" % rev - else: - args.append('-r' + str(rev)) - args.append(url) - self._authsvn('co', args) - - def update(self, rev='HEAD', interactive=True): - """ update working copy item to given revision. (None -> HEAD). """ - opts = ['-r', rev] - if not interactive: - opts.append("--non-interactive") - self._authsvn('up', opts) - - def write(self, content, mode='w'): - """ write content into local filesystem wc. """ - self.localpath.write(content, mode) - - def dirpath(self, *args): - """ return the directory Path of the current Path. """ - return self.__class__(self.localpath.dirpath(*args), auth=self.auth) - - def _ensuredirs(self): - parent = self.dirpath() - if parent.check(dir=0): - parent._ensuredirs() - if self.check(dir=0): - self.mkdir() - return self - - def ensure(self, *args, **kwargs): - """ ensure that an args-joined path exists (by default as - a file). if you specify a keyword argument 'directory=True' - then the path is forced to be a directory path. - """ - p = self.join(*args) - if p.check(): - if p.check(versioned=False): - p.add() - return p - if kwargs.get('dir', 0): - return p._ensuredirs() - parent = p.dirpath() - parent._ensuredirs() - p.write("") - p.add() - return p - - def mkdir(self, *args): - """ create & return the directory joined with args. """ - if args: - return self.join(*args).mkdir() - else: - self._svn('mkdir') - return self - - def add(self): - """ add ourself to svn """ - self._svn('add') - - def remove(self, rec=1, force=1): - """ remove a file or a directory tree. 'rec'ursive is - ignored and considered always true (because of - underlying svn semantics. - """ - assert rec, "svn cannot remove non-recursively" - if not self.check(versioned=True): - # not added to svn (anymore?), just remove - py.path.local(self).remove() - return - flags = [] - if force: - flags.append('--force') - self._svn('remove', *flags) - - def copy(self, target): - """ copy path to target.""" - py.process.cmdexec("svn copy %s %s" %(str(self), str(target))) - - def rename(self, target): - """ rename this path to target. """ - py.process.cmdexec("svn move --force %s %s" %(str(self), str(target))) - - def lock(self): - """ set a lock (exclusive) on the resource """ - out = self._authsvn('lock').strip() - if not out: - # warning or error, raise exception - raise ValueError("unknown error in svn lock command") - - def unlock(self): - """ unset a previously set lock """ - out = self._authsvn('unlock').strip() - if out.startswith('svn:'): - # warning or error, raise exception - raise Exception(out[4:]) - - def cleanup(self): - """ remove any locks from the resource """ - # XXX should be fixed properly!!! - try: - self.unlock() - except: - pass - - def status(self, updates=0, rec=0, externals=0): - """ return (collective) Status object for this file. """ - # http://svnbook.red-bean.com/book.html#svn-ch-3-sect-4.3.1 - # 2201 2192 jum test - # XXX - if externals: - raise ValueError("XXX cannot perform status() " - "on external items yet") - else: - #1.2 supports: externals = '--ignore-externals' - externals = '' - if rec: - rec= '' - else: - rec = '--non-recursive' - - # XXX does not work on all subversion versions - #if not externals: - # externals = '--ignore-externals' - - if updates: - updates = '-u' - else: - updates = '' - - try: - cmd = 'status -v --xml --no-ignore %s %s %s' % ( - updates, rec, externals) - out = self._authsvn(cmd) - except py.process.cmdexec.Error: - cmd = 'status -v --no-ignore %s %s %s' % ( - updates, rec, externals) - out = self._authsvn(cmd) - rootstatus = WCStatus(self).fromstring(out, self) - else: - rootstatus = XMLWCStatus(self).fromstring(out, self) - return rootstatus - - def diff(self, rev=None): - """ return a diff of the current path against revision rev (defaulting - to the last one). - """ - args = [] - if rev is not None: - args.append("-r %d" % rev) - out = self._authsvn('diff', args) - return out - - def blame(self): - """ return a list of tuples of three elements: - (revision, commiter, line) - """ - out = self._svn('blame') - result = [] - blamelines = out.splitlines() - reallines = py.path.svnurl(self.url).readlines() - for i, (blameline, line) in enumerate( - zip(blamelines, reallines)): - m = rex_blame.match(blameline) - if not m: - raise ValueError("output line %r of svn blame does not match " - "expected format" % (line, )) - rev, name, _ = m.groups() - result.append((int(rev), name, line)) - return result - - _rex_commit = re.compile(r'.*Committed revision (\d+)\.$', re.DOTALL) - def commit(self, msg='', rec=1): - """ commit with support for non-recursive commits """ - # XXX i guess escaping should be done better here?!? - cmd = 'commit -m "%s" --force-log' % (msg.replace('"', '\\"'),) - if not rec: - cmd += ' -N' - out = self._authsvn(cmd) - try: - del cache.info[self] - except KeyError: - pass - if out: - m = self._rex_commit.match(out) - return int(m.group(1)) - - def propset(self, name, value, *args): - """ set property name to value on this path. """ - d = py.path.local.mkdtemp() - try: - p = d.join('value') - p.write(value) - self._svn('propset', name, '--file', str(p), *args) - finally: - d.remove() - - def propget(self, name): - """ get property name on this path. """ - res = self._svn('propget', name) - return res[:-1] # strip trailing newline - - def propdel(self, name): - """ delete property name on this path. """ - res = self._svn('propdel', name) - return res[:-1] # strip trailing newline - - def proplist(self, rec=0): - """ return a mapping of property names to property values. -If rec is True, then return a dictionary mapping sub-paths to such mappings. -""" - if rec: - res = self._svn('proplist -R') - return make_recursive_propdict(self, res) - else: - res = self._svn('proplist') - lines = res.split('\n') - lines = [x.strip() for x in lines[1:]] - return PropListDict(self, lines) - - def revert(self, rec=0): - """ revert the local changes of this path. if rec is True, do so -recursively. """ - if rec: - result = self._svn('revert -R') - else: - result = self._svn('revert') - return result - - def new(self, **kw): - """ create a modified version of this path. A 'rev' argument - indicates a new revision. - the following keyword arguments modify various path parts: - - http://host.com/repo/path/file.ext - |-----------------------| dirname - |------| basename - |--| purebasename - |--| ext - """ - if kw: - localpath = self.localpath.new(**kw) - else: - localpath = self.localpath - return self.__class__(localpath, auth=self.auth) - - def join(self, *args, **kwargs): - """ return a new Path (with the same revision) which is composed - of the self Path followed by 'args' path components. - """ - if not args: - return self - localpath = self.localpath.join(*args, **kwargs) - return self.__class__(localpath, auth=self.auth) - - def info(self, usecache=1): - """ return an Info structure with svn-provided information. """ - info = usecache and cache.info.get(self) - if not info: - try: - output = self._svn('info') - except py.process.cmdexec.Error: - e = sys.exc_info()[1] - if e.err.find('Path is not a working copy directory') != -1: - raise py.error.ENOENT(self, e.err) - elif e.err.find("is not under version control") != -1: - raise py.error.ENOENT(self, e.err) - raise - # XXX SVN 1.3 has output on stderr instead of stdout (while it does - # return 0!), so a bit nasty, but we assume no output is output - # to stderr... - if (output.strip() == '' or - output.lower().find('not a versioned resource') != -1): - raise py.error.ENOENT(self, output) - info = InfoSvnWCCommand(output) - - # Can't reliably compare on Windows without access to win32api - if sys.platform != 'win32': - if info.path != self.localpath: - raise py.error.ENOENT(self, "not a versioned resource:" + - " %s != %s" % (info.path, self.localpath)) - cache.info[self] = info - return info - - def listdir(self, fil=None, sort=None): - """ return a sequence of Paths. - - listdir will return either a tuple or a list of paths - depending on implementation choices. - """ - if isinstance(fil, str): - fil = common.FNMatcher(fil) - # XXX unify argument naming with LocalPath.listdir - def notsvn(path): - return path.basename != '.svn' - - paths = [] - for localpath in self.localpath.listdir(notsvn): - p = self.__class__(localpath, auth=self.auth) - if notsvn(p) and (not fil or fil(p)): - paths.append(p) - self._sortlist(paths, sort) - return paths - - def open(self, mode='r'): - """ return an opened file with the given mode. """ - return open(self.strpath, mode) - - def _getbyspec(self, spec): - return self.localpath._getbyspec(spec) - - class Checkers(py.path.local.Checkers): - def __init__(self, path): - self.svnwcpath = path - self.path = path.localpath - def versioned(self): - try: - s = self.svnwcpath.info() - except (py.error.ENOENT, py.error.EEXIST): - return False - except py.process.cmdexec.Error: - e = sys.exc_info()[1] - if e.err.find('is not a working copy')!=-1: - return False - if e.err.lower().find('not a versioned resource') != -1: - return False - raise - else: - return True - - def log(self, rev_start=None, rev_end=1, verbose=False): - """ return a list of LogEntry instances for this path. -rev_start is the starting revision (defaulting to the first one). -rev_end is the last revision (defaulting to HEAD). -if verbose is True, then the LogEntry instances also know which files changed. -""" - assert self.check() # make it simpler for the pipe - rev_start = rev_start is None and "HEAD" or rev_start - rev_end = rev_end is None and "HEAD" or rev_end - if rev_start == "HEAD" and rev_end == 1: - rev_opt = "" - else: - rev_opt = "-r %s:%s" % (rev_start, rev_end) - verbose_opt = verbose and "-v" or "" - locale_env = fixlocale() - # some blather on stderr - auth_opt = self._makeauthoptions() - #stdin, stdout, stderr = os.popen3(locale_env + - # 'svn log --xml %s %s %s "%s"' % ( - # rev_opt, verbose_opt, auth_opt, - # self.strpath)) - cmd = locale_env + 'svn log --xml %s %s %s "%s"' % ( - rev_opt, verbose_opt, auth_opt, self.strpath) - - popen = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - ) - stdout, stderr = popen.communicate() - stdout = py.builtin._totext(stdout, sys.getdefaultencoding()) - minidom,ExpatError = importxml() - try: - tree = minidom.parseString(stdout) - except ExpatError: - raise ValueError('no such revision') - result = [] - for logentry in filter(None, tree.firstChild.childNodes): - if logentry.nodeType == logentry.ELEMENT_NODE: - result.append(LogEntry(logentry)) - return result - - def size(self): - """ Return the size of the file content of the Path. """ - return self.info().size - - def mtime(self): - """ Return the last modification time of the file. """ - return self.info().mtime - - def __hash__(self): - return hash((self.strpath, self.__class__, self.auth)) - - -class WCStatus: - attrnames = ('modified','added', 'conflict', 'unchanged', 'external', - 'deleted', 'prop_modified', 'unknown', 'update_available', - 'incomplete', 'kindmismatch', 'ignored', 'locked', 'replaced' - ) - - def __init__(self, wcpath, rev=None, modrev=None, author=None): - self.wcpath = wcpath - self.rev = rev - self.modrev = modrev - self.author = author - - for name in self.attrnames: - setattr(self, name, []) - - def allpath(self, sort=True, **kw): - d = {} - for name in self.attrnames: - if name not in kw or kw[name]: - for path in getattr(self, name): - d[path] = 1 - l = d.keys() - if sort: - l.sort() - return l - - # XXX a bit scary to assume there's always 2 spaces between username and - # path, however with win32 allowing spaces in user names there doesn't - # seem to be a more solid approach :( - _rex_status = re.compile(r'\s+(\d+|-)\s+(\S+)\s+(.+?)\s{2,}(.*)') - - def fromstring(data, rootwcpath, rev=None, modrev=None, author=None): - """ return a new WCStatus object from data 's' - """ - rootstatus = WCStatus(rootwcpath, rev, modrev, author) - update_rev = None - for line in data.split('\n'): - if not line.strip(): - continue - #print "processing %r" % line - flags, rest = line[:8], line[8:] - # first column - c0,c1,c2,c3,c4,c5,x6,c7 = flags - #if '*' in line: - # print "flags", repr(flags), "rest", repr(rest) - - if c0 in '?XI': - fn = line.split(None, 1)[1] - if c0 == '?': - wcpath = rootwcpath.join(fn, abs=1) - rootstatus.unknown.append(wcpath) - elif c0 == 'X': - wcpath = rootwcpath.__class__( - rootwcpath.localpath.join(fn, abs=1), - auth=rootwcpath.auth) - rootstatus.external.append(wcpath) - elif c0 == 'I': - wcpath = rootwcpath.join(fn, abs=1) - rootstatus.ignored.append(wcpath) - - continue - - #elif c0 in '~!' or c4 == 'S': - # raise NotImplementedError("received flag %r" % c0) - - m = WCStatus._rex_status.match(rest) - if not m: - if c7 == '*': - fn = rest.strip() - wcpath = rootwcpath.join(fn, abs=1) - rootstatus.update_available.append(wcpath) - continue - if line.lower().find('against revision:')!=-1: - update_rev = int(rest.split(':')[1].strip()) - continue - if line.lower().find('status on external') > -1: - # XXX not sure what to do here... perhaps we want to - # store some state instead of just continuing, as right - # now it makes the top-level external get added twice - # (once as external, once as 'normal' unchanged item) - # because of the way SVN presents external items - continue - # keep trying - raise ValueError("could not parse line %r" % line) - else: - rev, modrev, author, fn = m.groups() - wcpath = rootwcpath.join(fn, abs=1) - #assert wcpath.check() - if c0 == 'M': - assert wcpath.check(file=1), "didn't expect a directory with changed content here" - rootstatus.modified.append(wcpath) - elif c0 == 'A' or c3 == '+' : - rootstatus.added.append(wcpath) - elif c0 == 'D': - rootstatus.deleted.append(wcpath) - elif c0 == 'C': - rootstatus.conflict.append(wcpath) - elif c0 == '~': - rootstatus.kindmismatch.append(wcpath) - elif c0 == '!': - rootstatus.incomplete.append(wcpath) - elif c0 == 'R': - rootstatus.replaced.append(wcpath) - elif not c0.strip(): - rootstatus.unchanged.append(wcpath) - else: - raise NotImplementedError("received flag %r" % c0) - - if c1 == 'M': - rootstatus.prop_modified.append(wcpath) - # XXX do we cover all client versions here? - if c2 == 'L' or c5 == 'K': - rootstatus.locked.append(wcpath) - if c7 == '*': - rootstatus.update_available.append(wcpath) - - if wcpath == rootwcpath: - rootstatus.rev = rev - rootstatus.modrev = modrev - rootstatus.author = author - if update_rev: - rootstatus.update_rev = update_rev - continue - return rootstatus - fromstring = staticmethod(fromstring) - -class XMLWCStatus(WCStatus): - def fromstring(data, rootwcpath, rev=None, modrev=None, author=None): - """ parse 'data' (XML string as outputted by svn st) into a status obj - """ - # XXX for externals, the path is shown twice: once - # with external information, and once with full info as if - # the item was a normal non-external... the current way of - # dealing with this issue is by ignoring it - this does make - # externals appear as external items as well as 'normal', - # unchanged ones in the status object so this is far from ideal - rootstatus = WCStatus(rootwcpath, rev, modrev, author) - update_rev = None - minidom, ExpatError = importxml() - try: - doc = minidom.parseString(data) - except ExpatError: - e = sys.exc_info()[1] - raise ValueError(str(e)) - urevels = doc.getElementsByTagName('against') - if urevels: - rootstatus.update_rev = urevels[-1].getAttribute('revision') - for entryel in doc.getElementsByTagName('entry'): - path = entryel.getAttribute('path') - statusel = entryel.getElementsByTagName('wc-status')[0] - itemstatus = statusel.getAttribute('item') - - if itemstatus == 'unversioned': - wcpath = rootwcpath.join(path, abs=1) - rootstatus.unknown.append(wcpath) - continue - elif itemstatus == 'external': - wcpath = rootwcpath.__class__( - rootwcpath.localpath.join(path, abs=1), - auth=rootwcpath.auth) - rootstatus.external.append(wcpath) - continue - elif itemstatus == 'ignored': - wcpath = rootwcpath.join(path, abs=1) - rootstatus.ignored.append(wcpath) - continue - elif itemstatus == 'incomplete': - wcpath = rootwcpath.join(path, abs=1) - rootstatus.incomplete.append(wcpath) - continue - - rev = statusel.getAttribute('revision') - if itemstatus == 'added' or itemstatus == 'none': - rev = '0' - modrev = '?' - author = '?' - date = '' - elif itemstatus == "replaced": - pass - else: - #print entryel.toxml() - commitel = entryel.getElementsByTagName('commit')[0] - if commitel: - modrev = commitel.getAttribute('revision') - author = '' - author_els = commitel.getElementsByTagName('author') - if author_els: - for c in author_els[0].childNodes: - author += c.nodeValue - date = '' - for c in commitel.getElementsByTagName('date')[0]\ - .childNodes: - date += c.nodeValue - - wcpath = rootwcpath.join(path, abs=1) - - assert itemstatus != 'modified' or wcpath.check(file=1), ( - 'did\'t expect a directory with changed content here') - - itemattrname = { - 'normal': 'unchanged', - 'unversioned': 'unknown', - 'conflicted': 'conflict', - 'none': 'added', - }.get(itemstatus, itemstatus) - - attr = getattr(rootstatus, itemattrname) - attr.append(wcpath) - - propsstatus = statusel.getAttribute('props') - if propsstatus not in ('none', 'normal'): - rootstatus.prop_modified.append(wcpath) - - if wcpath == rootwcpath: - rootstatus.rev = rev - rootstatus.modrev = modrev - rootstatus.author = author - rootstatus.date = date - - # handle repos-status element (remote info) - rstatusels = entryel.getElementsByTagName('repos-status') - if rstatusels: - rstatusel = rstatusels[0] - ritemstatus = rstatusel.getAttribute('item') - if ritemstatus in ('added', 'modified'): - rootstatus.update_available.append(wcpath) - - lockels = entryel.getElementsByTagName('lock') - if len(lockels): - rootstatus.locked.append(wcpath) - - return rootstatus - fromstring = staticmethod(fromstring) - -class InfoSvnWCCommand: - def __init__(self, output): - # Path: test - # URL: http://codespeak.net/svn/std.path/trunk/dist/std.path/test - # Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada - # Revision: 2151 - # Node Kind: directory - # Schedule: normal - # Last Changed Author: hpk - # Last Changed Rev: 2100 - # Last Changed Date: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003) - # Properties Last Updated: 2003-11-03 14:47:48 +0100 (Mon, 03 Nov 2003) - - d = {} - for line in output.split('\n'): - if not line.strip(): - continue - key, value = line.split(':', 1) - key = key.lower().replace(' ', '') - value = value.strip() - d[key] = value - try: - self.url = d['url'] - except KeyError: - raise ValueError("Not a versioned resource") - #raise ValueError, "Not a versioned resource %r" % path - self.kind = d['nodekind'] == 'directory' and 'dir' or d['nodekind'] - try: - self.rev = int(d['revision']) - except KeyError: - self.rev = None - - self.path = py.path.local(d['path']) - self.size = self.path.size() - if 'lastchangedrev' in d: - self.created_rev = int(d['lastchangedrev']) - if 'lastchangedauthor' in d: - self.last_author = d['lastchangedauthor'] - if 'lastchangeddate' in d: - self.mtime = parse_wcinfotime(d['lastchangeddate']) - self.time = self.mtime * 1000000 - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - -def parse_wcinfotime(timestr): - """ Returns seconds since epoch, UTC. """ - # example: 2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003) - m = re.match(r'(\d+-\d+-\d+ \d+:\d+:\d+) ([+-]\d+) .*', timestr) - if not m: - raise ValueError("timestring %r does not match" % timestr) - timestr, timezone = m.groups() - # do not handle timezone specially, return value should be UTC - parsedtime = time.strptime(timestr, "%Y-%m-%d %H:%M:%S") - return calendar.timegm(parsedtime) - -def make_recursive_propdict(wcroot, - output, - rex = re.compile("Properties on '(.*)':")): - """ Return a dictionary of path->PropListDict mappings. """ - lines = [x for x in output.split('\n') if x] - pdict = {} - while lines: - line = lines.pop(0) - m = rex.match(line) - if not m: - raise ValueError("could not parse propget-line: %r" % line) - path = m.groups()[0] - wcpath = wcroot.join(path, abs=1) - propnames = [] - while lines and lines[0].startswith(' '): - propname = lines.pop(0).strip() - propnames.append(propname) - assert propnames, "must have found properties!" - pdict[wcpath] = PropListDict(wcpath, propnames) - return pdict - - -def importxml(cache=[]): - if cache: - return cache - from xml.dom import minidom - from xml.parsers.expat import ExpatError - cache.extend([minidom, ExpatError]) - return cache - -class LogEntry: - def __init__(self, logentry): - self.rev = int(logentry.getAttribute('revision')) - for lpart in filter(None, logentry.childNodes): - if lpart.nodeType == lpart.ELEMENT_NODE: - if lpart.nodeName == 'author': - self.author = lpart.firstChild.nodeValue - elif lpart.nodeName == 'msg': - if lpart.firstChild: - self.msg = lpart.firstChild.nodeValue - else: - self.msg = '' - elif lpart.nodeName == 'date': - #2003-07-29T20:05:11.598637Z - timestr = lpart.firstChild.nodeValue - self.date = parse_apr_time(timestr) - elif lpart.nodeName == 'paths': - self.strpaths = [] - for ppart in filter(None, lpart.childNodes): - if ppart.nodeType == ppart.ELEMENT_NODE: - self.strpaths.append(PathEntry(ppart)) - def __repr__(self): - return '<Logentry rev=%d author=%s date=%s>' % ( - self.rev, self.author, self.date) - - diff --git a/tests/wpt/tests/tools/third_party/py/py/_process/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_process/__init__.py deleted file mode 100644 index 86c714ad1ae..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_process/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" high-level sub-process handling """ diff --git a/tests/wpt/tests/tools/third_party/py/py/_process/cmdexec.py b/tests/wpt/tests/tools/third_party/py/py/_process/cmdexec.py deleted file mode 100644 index f83a2494029..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_process/cmdexec.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -import subprocess -import py -from subprocess import Popen, PIPE - -def cmdexec(cmd): - """ return unicode output of executing 'cmd' in a separate process. - - raise cmdexec.Error exeception if the command failed. - the exception will provide an 'err' attribute containing - the error-output from the command. - if the subprocess module does not provide a proper encoding/unicode strings - sys.getdefaultencoding() will be used, if that does not exist, 'UTF-8'. - """ - process = subprocess.Popen(cmd, shell=True, - universal_newlines=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = process.communicate() - if sys.version_info[0] < 3: # on py3 we get unicode strings, on py2 not - try: - default_encoding = sys.getdefaultencoding() # jython may not have it - except AttributeError: - default_encoding = sys.stdout.encoding or 'UTF-8' - out = unicode(out, process.stdout.encoding or default_encoding) - err = unicode(err, process.stderr.encoding or default_encoding) - status = process.poll() - if status: - raise ExecutionFailed(status, status, cmd, out, err) - return out - -class ExecutionFailed(py.error.Error): - def __init__(self, status, systemstatus, cmd, out, err): - Exception.__init__(self) - self.status = status - self.systemstatus = systemstatus - self.cmd = cmd - self.err = err - self.out = out - - def __str__(self): - return "ExecutionFailed: %d %s\n%s" %(self.status, self.cmd, self.err) - -# export the exception under the name 'py.process.cmdexec.Error' -cmdexec.Error = ExecutionFailed -try: - ExecutionFailed.__module__ = 'py.process.cmdexec' - ExecutionFailed.__name__ = 'Error' -except (AttributeError, TypeError): - pass diff --git a/tests/wpt/tests/tools/third_party/py/py/_process/forkedfunc.py b/tests/wpt/tests/tools/third_party/py/py/_process/forkedfunc.py deleted file mode 100644 index 1c285306884..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_process/forkedfunc.py +++ /dev/null @@ -1,120 +0,0 @@ - -""" - ForkedFunc provides a way to run a function in a forked process - and get at its return value, stdout and stderr output as well - as signals and exitstatusus. -""" - -import py -import os -import sys -import marshal - - -def get_unbuffered_io(fd, filename): - f = open(str(filename), "w") - if fd != f.fileno(): - os.dup2(f.fileno(), fd) - class AutoFlush: - def write(self, data): - f.write(data) - f.flush() - def __getattr__(self, name): - return getattr(f, name) - return AutoFlush() - - -class ForkedFunc: - EXITSTATUS_EXCEPTION = 3 - - - def __init__(self, fun, args=None, kwargs=None, nice_level=0, - child_on_start=None, child_on_exit=None): - if args is None: - args = [] - if kwargs is None: - kwargs = {} - self.fun = fun - self.args = args - self.kwargs = kwargs - self.tempdir = tempdir = py.path.local.mkdtemp() - self.RETVAL = tempdir.ensure('retval') - self.STDOUT = tempdir.ensure('stdout') - self.STDERR = tempdir.ensure('stderr') - - pid = os.fork() - if pid: # in parent process - self.pid = pid - else: # in child process - self.pid = None - self._child(nice_level, child_on_start, child_on_exit) - - def _child(self, nice_level, child_on_start, child_on_exit): - # right now we need to call a function, but first we need to - # map all IO that might happen - sys.stdout = stdout = get_unbuffered_io(1, self.STDOUT) - sys.stderr = stderr = get_unbuffered_io(2, self.STDERR) - retvalf = self.RETVAL.open("wb") - EXITSTATUS = 0 - try: - if nice_level: - os.nice(nice_level) - try: - if child_on_start is not None: - child_on_start() - retval = self.fun(*self.args, **self.kwargs) - retvalf.write(marshal.dumps(retval)) - if child_on_exit is not None: - child_on_exit() - except: - excinfo = py.code.ExceptionInfo() - stderr.write(str(excinfo._getreprcrash())) - EXITSTATUS = self.EXITSTATUS_EXCEPTION - finally: - stdout.close() - stderr.close() - retvalf.close() - os.close(1) - os.close(2) - os._exit(EXITSTATUS) - - def waitfinish(self, waiter=os.waitpid): - pid, systemstatus = waiter(self.pid, 0) - if systemstatus: - if os.WIFSIGNALED(systemstatus): - exitstatus = os.WTERMSIG(systemstatus) + 128 - else: - exitstatus = os.WEXITSTATUS(systemstatus) - else: - exitstatus = 0 - signal = systemstatus & 0x7f - if not exitstatus and not signal: - retval = self.RETVAL.open('rb') - try: - retval_data = retval.read() - finally: - retval.close() - retval = marshal.loads(retval_data) - else: - retval = None - stdout = self.STDOUT.read() - stderr = self.STDERR.read() - self._removetemp() - return Result(exitstatus, signal, retval, stdout, stderr) - - def _removetemp(self): - if self.tempdir.check(): - self.tempdir.remove() - - def __del__(self): - if self.pid is not None: # only clean up in main process - self._removetemp() - - -class Result(object): - def __init__(self, exitstatus, signal, retval, stdout, stderr): - self.exitstatus = exitstatus - self.signal = signal - self.retval = retval - self.out = stdout - self.err = stderr diff --git a/tests/wpt/tests/tools/third_party/py/py/_process/killproc.py b/tests/wpt/tests/tools/third_party/py/py/_process/killproc.py deleted file mode 100644 index 18e8310b5f6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_process/killproc.py +++ /dev/null @@ -1,23 +0,0 @@ -import py -import os, sys - -if sys.platform == "win32" or getattr(os, '_name', '') == 'nt': - try: - import ctypes - except ImportError: - def dokill(pid): - py.process.cmdexec("taskkill /F /PID %d" %(pid,)) - else: - def dokill(pid): - PROCESS_TERMINATE = 1 - handle = ctypes.windll.kernel32.OpenProcess( - PROCESS_TERMINATE, False, pid) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) -else: - def dokill(pid): - os.kill(pid, 15) - -def kill(pid): - """ kill process by id. """ - dokill(pid) diff --git a/tests/wpt/tests/tools/third_party/py/py/_std.py b/tests/wpt/tests/tools/third_party/py/py/_std.py deleted file mode 100644 index 66adb7b0239..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_std.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys -import warnings - - -class PyStdIsDeprecatedWarning(DeprecationWarning): - pass - - -class Std(object): - """ makes top-level python modules available as an attribute, - importing them on first access. - """ - - def __init__(self): - self.__dict__ = sys.modules - - def __getattr__(self, name): - warnings.warn("py.std is deprecated, please import %s directly" % name, - category=PyStdIsDeprecatedWarning, - stacklevel=2) - try: - m = __import__(name) - except ImportError: - raise AttributeError("py.std: could not import %s" % name) - return m - -std = Std() diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e38a3..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE deleted file mode 100644 index ff33b8f7ca0..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/LICENSE +++ /dev/null @@ -1,18 +0,0 @@ - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA deleted file mode 100644 index 7eea770a02b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/METADATA +++ /dev/null @@ -1,125 +0,0 @@ -Metadata-Version: 2.1 -Name: apipkg -Version: 2.0.0 -Summary: apipkg: namespace control and lazy-import mechanism -Home-page: https://github.com/pytest-dev/apipkg -Author: holger krekel -Maintainer: Ronny Pfannschmidt -Maintainer-email: opensource@ronnypfannschmidt.de -License: MIT -Platform: unix -Platform: linux -Platform: osx -Platform: cygwin -Platform: win32 -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: MacOS :: MacOS X -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Topic :: Software Development :: Libraries -Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 -Description-Content-Type: text/x-rst -License-File: LICENSE - -Welcome to apipkg ! -------------------- - -With apipkg you can control the exported namespace of a Python package and -greatly reduce the number of imports for your users. -It is a `small pure Python module`_ that works on CPython 2.7 and 3.4+, -Jython and PyPy. It cooperates well with Python's ``help()`` system, -custom importers (PEP302) and common command-line completion tools. - -Usage is very simple: you can require 'apipkg' as a dependency or you -can copy paste the ~200 lines of code into your project. - - -Tutorial example -------------------- - -Here is a simple ``mypkg`` package that specifies one namespace -and exports two objects imported from different modules:: - - - # mypkg/__init__.py - import apipkg - apipkg.initpkg(__name__, { - 'path': { - 'Class1': "_mypkg.somemodule:Class1", - 'clsattr': "_mypkg.othermodule:Class2.attr", - } - } - -The package is initialized with a dictionary as namespace. - -You need to create a ``_mypkg`` package with a ``somemodule.py`` -and ``othermodule.py`` containing the respective classes. -The ``_mypkg`` is not special - it's a completely -regular Python package. - -Namespace dictionaries contain ``name: value`` mappings -where the value may be another namespace dictionary or -a string specifying an import location. On accessing -an namespace attribute an import will be performed:: - - >>> import mypkg - >>> mypkg.path - <ApiModule 'mypkg.path'> - >>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now - <class _mypkg.somemodule.Class1 at 0xb7d428fc> - >>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now - 4 # the value of _mypkg.othermodule.Class2.attr - -The ``mypkg.path`` namespace and its two entries are -loaded when they are accessed. This means: - -* lazy loading - only what is actually needed is ever loaded - -* only the root "mypkg" ever needs to be imported to get - access to the complete functionality - -* the underlying modules are also accessible, for example:: - - from mypkg.sub import Class1 - - -Including apipkg in your package --------------------------------------- - -If you don't want to add an ``apipkg`` dependency to your package you -can copy the `apipkg.py`_ file somewhere to your own package, -for example ``_mypkg/apipkg.py`` in the above example. You -then import the ``initpkg`` function from that new place and -are good to go. - -.. _`small pure Python module`: -.. _`apipkg.py`: https://github.com/pytest-dev/apipkg/blob/master/src/apipkg/__init__.py - -Feedback? ------------------------ - -If you have questions you are welcome to - -* join the **#pytest** channel on irc.libera.chat_ - (using an IRC client, via webchat_, or via Matrix_). -* create an issue on the bugtracker_ - -.. _irc.libera.chat: ircs://irc.libera.chat:6697/#pytest -.. _webchat: https://web.libera.chat/#pytest -.. _matrix: https://matrix.to/#/%23pytest:libera.chat -.. _bugtracker: https://github.com/pytest-dev/apipkg/issues - - diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD deleted file mode 100644 index 357b8b9c729..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/RECORD +++ /dev/null @@ -1,11 +0,0 @@ -apipkg-2.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-apipkg-2.0.0.dist-info/LICENSE,sha256=6J7tEHTTqUMZi6E5uAhE9bRFuGC7p0qK6twGEFZhZOo,1054
-apipkg-2.0.0.dist-info/METADATA,sha256=GqNwkxraK5UTxObLVXTLc2UqktOPwZnKqdk2ThzHX0A,4292
-apipkg-2.0.0.dist-info/RECORD,,
-apipkg-2.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-apipkg-2.0.0.dist-info/WHEEL,sha256=WzZ8cwjh8l0jtULNjYq1Hpr-WCqCRgPr--TX4P5I1Wo,110
-apipkg-2.0.0.dist-info/top_level.txt,sha256=3TGS6nmN7kjxhUK4LpPCB3QkQI34QYGrT0ZQGWajoZ8,7
-apipkg/__init__.py,sha256=gpbD3O57S9f-LsO2e-XwI6IGISayicfnCq3B5y_8frg,6978
-apipkg/__pycache__/__init__.cpython-39.pyc,,
-apipkg/__pycache__/version.cpython-39.pyc,,
-apipkg/version.py,sha256=bgZFg-f3UKhgE-z2w8RoFrwqRBzJBZkM4_jKFiYB9eU,142
diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL deleted file mode 100644 index b733a60d379..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/WHEEL +++ /dev/null @@ -1,6 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.37.0) -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any - diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt deleted file mode 100644 index e2221c8f9e9..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -apipkg diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py deleted file mode 100644 index 350d8c4b07a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/__init__.py +++ /dev/null @@ -1,217 +0,0 @@ -""" -apipkg: control the exported namespace of a Python package. - -see https://pypi.python.org/pypi/apipkg - -(c) holger krekel, 2009 - MIT license -""" -import os -import sys -from types import ModuleType - -from .version import version as __version__ # NOQA:F401 - - -def _py_abspath(path): - """ - special version of abspath - that will leave paths from jython jars alone - """ - if path.startswith("__pyclasspath__"): - - return path - else: - return os.path.abspath(path) - - -def distribution_version(name): - """try to get the version of the named distribution, - returs None on failure""" - from pkg_resources import get_distribution, DistributionNotFound - - try: - dist = get_distribution(name) - except DistributionNotFound: - pass - else: - return dist.version - - -def initpkg(pkgname, exportdefs, attr=None, eager=False): - """ initialize given package from the export definitions. """ - attr = attr or {} - oldmod = sys.modules.get(pkgname) - d = {} - f = getattr(oldmod, "__file__", None) - if f: - f = _py_abspath(f) - d["__file__"] = f - if hasattr(oldmod, "__version__"): - d["__version__"] = oldmod.__version__ - if hasattr(oldmod, "__loader__"): - d["__loader__"] = oldmod.__loader__ - if hasattr(oldmod, "__path__"): - d["__path__"] = [_py_abspath(p) for p in oldmod.__path__] - if hasattr(oldmod, "__package__"): - d["__package__"] = oldmod.__package__ - if "__doc__" not in exportdefs and getattr(oldmod, "__doc__", None): - d["__doc__"] = oldmod.__doc__ - d["__spec__"] = getattr(oldmod, "__spec__", None) - d.update(attr) - if hasattr(oldmod, "__dict__"): - oldmod.__dict__.update(d) - mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) - sys.modules[pkgname] = mod - # eagerload in bypthon to avoid their monkeypatching breaking packages - if "bpython" in sys.modules or eager: - for module in list(sys.modules.values()): - if isinstance(module, ApiModule): - module.__dict__ - return mod - - -def importobj(modpath, attrname): - """imports a module, then resolves the attrname on it""" - module = __import__(modpath, None, None, ["__doc__"]) - if not attrname: - return module - - retval = module - names = attrname.split(".") - for x in names: - retval = getattr(retval, x) - return retval - - -class ApiModule(ModuleType): - """the magical lazy-loading module standing""" - - def __docget(self): - try: - return self.__doc - except AttributeError: - if "__doc__" in self.__map__: - return self.__makeattr("__doc__") - - def __docset(self, value): - self.__doc = value - - __doc__ = property(__docget, __docset) - - def __init__(self, name, importspec, implprefix=None, attr=None): - self.__name__ = name - self.__all__ = [x for x in importspec if x != "__onfirstaccess__"] - self.__map__ = {} - self.__implprefix__ = implprefix or name - if attr: - for name, val in attr.items(): - # print "setting", self.__name__, name, val - setattr(self, name, val) - for name, importspec in importspec.items(): - if isinstance(importspec, dict): - subname = "{}.{}".format(self.__name__, name) - apimod = ApiModule(subname, importspec, implprefix) - sys.modules[subname] = apimod - setattr(self, name, apimod) - else: - parts = importspec.split(":") - modpath = parts.pop(0) - attrname = parts and parts[0] or "" - if modpath[0] == ".": - modpath = implprefix + modpath - - if not attrname: - subname = "{}.{}".format(self.__name__, name) - apimod = AliasModule(subname, modpath) - sys.modules[subname] = apimod - if "." not in name: - setattr(self, name, apimod) - else: - self.__map__[name] = (modpath, attrname) - - def __repr__(self): - repr_list = [] - if hasattr(self, "__version__"): - repr_list.append("version=" + repr(self.__version__)) - if hasattr(self, "__file__"): - repr_list.append("from " + repr(self.__file__)) - if repr_list: - return "<ApiModule {!r} {}>".format(self.__name__, " ".join(repr_list)) - return "<ApiModule {!r}>".format(self.__name__) - - def __makeattr(self, name): - """lazily compute value for name or raise AttributeError if unknown.""" - # print "makeattr", self.__name__, name - target = None - if "__onfirstaccess__" in self.__map__: - target = self.__map__.pop("__onfirstaccess__") - importobj(*target)() - try: - modpath, attrname = self.__map__[name] - except KeyError: - if target is not None and name != "__onfirstaccess__": - # retry, onfirstaccess might have set attrs - return getattr(self, name) - raise AttributeError(name) - else: - result = importobj(modpath, attrname) - setattr(self, name, result) - try: - del self.__map__[name] - except KeyError: - pass # in a recursive-import situation a double-del can happen - return result - - __getattr__ = __makeattr - - @property - def __dict__(self): - # force all the content of the module - # to be loaded when __dict__ is read - dictdescr = ModuleType.__dict__["__dict__"] - dict = dictdescr.__get__(self) - if dict is not None: - hasattr(self, "some") - for name in self.__all__: - try: - self.__makeattr(name) - except AttributeError: - pass - return dict - - -def AliasModule(modname, modpath, attrname=None): - mod = [] - - def getmod(): - if not mod: - x = importobj(modpath, None) - if attrname is not None: - x = getattr(x, attrname) - mod.append(x) - return mod[0] - - x = modpath + ("." + attrname if attrname else "") - repr_result = "<AliasModule {!r} for {!r}>".format(modname, x) - - class AliasModule(ModuleType): - def __repr__(self): - return repr_result - - def __getattribute__(self, name): - try: - return getattr(getmod(), name) - except ImportError: - if modpath == "pytest" and attrname is None: - # hack for pylibs py.test - return None - else: - raise - - def __setattr__(self, name, value): - setattr(getmod(), name, value) - - def __delattr__(self, name): - delattr(getmod(), name) - - return AliasModule(str(modname)) diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py deleted file mode 100644 index c5b4e0e79fa..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg/version.py +++ /dev/null @@ -1,5 +0,0 @@ -# coding: utf-8 -# file generated by setuptools_scm -# don't change, don't track in version control -version = '2.0.0' -version_tuple = (2, 0, 0) diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER deleted file mode 100644 index a1b589e38a3..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE deleted file mode 100644 index 31ecdfb1dbc..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA deleted file mode 100644 index c078a7532fb..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/METADATA +++ /dev/null @@ -1,78 +0,0 @@ -Metadata-Version: 2.1 -Name: iniconfig -Version: 1.1.1 -Summary: iniconfig: brain-dead simple config-ini parsing -Home-page: http://github.com/RonnyPfannschmidt/iniconfig -Author: Ronny Pfannschmidt, Holger Krekel -Author-email: opensource@ronnypfannschmidt.de, holger.krekel@gmail.com -License: MIT License -Platform: unix -Platform: linux -Platform: osx -Platform: cygwin -Platform: win32 -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: POSIX -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: MacOS :: MacOS X -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: Utilities -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 - -iniconfig: brain-dead simple parsing of ini files -======================================================= - -iniconfig is a small and simple INI-file parser module -having a unique set of features: - -* tested against Python2.4 across to Python3.2, Jython, PyPy -* maintains order of sections and entries -* supports multi-line values with or without line-continuations -* supports "#" comments everywhere -* raises errors with proper line-numbers -* no bells and whistles like automatic substitutions -* iniconfig raises an Error if two sections have the same name. - -If you encounter issues or have feature wishes please report them to: - - http://github.com/RonnyPfannschmidt/iniconfig/issues - -Basic Example -=================================== - -If you have an ini file like this:: - - # content of example.ini - [section1] # comment - name1=value1 # comment - name1b=value1,value2 # comment - - [section2] - name2= - line1 - line2 - -then you can do:: - - >>> import iniconfig - >>> ini = iniconfig.IniConfig("example.ini") - >>> ini['section1']['name1'] # raises KeyError if not exists - 'value1' - >>> ini.get('section1', 'name1b', [], lambda x: x.split(",")) - ['value1', 'value2'] - >>> ini.get('section1', 'notexist', [], lambda x: x.split(",")) - [] - >>> [x.name for x in list(ini)] - ['section1', 'section2'] - >>> list(list(ini)[0].items()) - [('name1', 'value1'), ('name1b', 'value1,value2')] - >>> 'section1' in ini - True - >>> 'inexistendsection' in ini - False - - diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD deleted file mode 100644 index 168233330b6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/RECORD +++ /dev/null @@ -1,11 +0,0 @@ -iniconfig-1.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-iniconfig-1.1.1.dist-info/LICENSE,sha256=KvaAw570k_uCgwNW0dPfGstaBgM8ui3sehniHKp3qGY,1061
-iniconfig-1.1.1.dist-info/METADATA,sha256=_4-oFKpRXuZv5rzepScpXRwhq6DzqsgbnA5ZpgMUMcs,2405
-iniconfig-1.1.1.dist-info/RECORD,,
-iniconfig-1.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-iniconfig-1.1.1.dist-info/WHEEL,sha256=ADKeyaGyKF5DwBNE0sRE5pvW-bSkFMJfBuhzZ3rceP4,110
-iniconfig-1.1.1.dist-info/top_level.txt,sha256=7KfM0fugdlToj9UW7enKXk2HYALQD8qHiyKtjhSzgN8,10
-iniconfig/__init__.py,sha256=-pBe5AF_6aAwo1CxJQ8i_zJq6ejc6IxHta7qk2tNJhY,5208
-iniconfig/__init__.pyi,sha256=-4KOctzq28ohRmTZsqlH6aylyFqsNKxYqtk1dteypi4,1205
-iniconfig/__pycache__/__init__.cpython-39.pyc,,
-iniconfig/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED deleted file mode 100644 index e69de29bb2d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/REQUESTED +++ /dev/null diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL deleted file mode 100644 index 6d38aa0601b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/WHEEL +++ /dev/null @@ -1,6 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.35.1) -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any - diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt deleted file mode 100644 index 9dda53692d2..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig-1.1.1.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -iniconfig diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py deleted file mode 100644 index 6ad9eaf868b..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.py +++ /dev/null @@ -1,165 +0,0 @@ -""" brain-dead simple parser for ini-style files. -(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed -""" -__all__ = ['IniConfig', 'ParseError'] - -COMMENTCHARS = "#;" - - -class ParseError(Exception): - def __init__(self, path, lineno, msg): - Exception.__init__(self, path, lineno, msg) - self.path = path - self.lineno = lineno - self.msg = msg - - def __str__(self): - return "%s:%s: %s" % (self.path, self.lineno+1, self.msg) - - -class SectionWrapper(object): - def __init__(self, config, name): - self.config = config - self.name = name - - def lineof(self, name): - return self.config.lineof(self.name, name) - - def get(self, key, default=None, convert=str): - return self.config.get(self.name, key, - convert=convert, default=default) - - def __getitem__(self, key): - return self.config.sections[self.name][key] - - def __iter__(self): - section = self.config.sections.get(self.name, []) - - def lineof(key): - return self.config.lineof(self.name, key) - for name in sorted(section, key=lineof): - yield name - - def items(self): - for name in self: - yield name, self[name] - - -class IniConfig(object): - def __init__(self, path, data=None): - self.path = str(path) # convenience - if data is None: - f = open(self.path) - try: - tokens = self._parse(iter(f)) - finally: - f.close() - else: - tokens = self._parse(data.splitlines(True)) - - self._sources = {} - self.sections = {} - - for lineno, section, name, value in tokens: - if section is None: - self._raise(lineno, 'no section header defined') - self._sources[section, name] = lineno - if name is None: - if section in self.sections: - self._raise(lineno, 'duplicate section %r' % (section, )) - self.sections[section] = {} - else: - if name in self.sections[section]: - self._raise(lineno, 'duplicate name %r' % (name, )) - self.sections[section][name] = value - - def _raise(self, lineno, msg): - raise ParseError(self.path, lineno, msg) - - def _parse(self, line_iter): - result = [] - section = None - for lineno, line in enumerate(line_iter): - name, data = self._parseline(line, lineno) - # new value - if name is not None and data is not None: - result.append((lineno, section, name, data)) - # new section - elif name is not None and data is None: - if not name: - self._raise(lineno, 'empty section name') - section = name - result.append((lineno, section, None, None)) - # continuation - elif name is None and data is not None: - if not result: - self._raise(lineno, 'unexpected value continuation') - last = result.pop() - last_name, last_data = last[-2:] - if last_name is None: - self._raise(lineno, 'unexpected value continuation') - - if last_data: - data = '%s\n%s' % (last_data, data) - result.append(last[:-1] + (data,)) - return result - - def _parseline(self, line, lineno): - # blank lines - if iscommentline(line): - line = "" - else: - line = line.rstrip() - if not line: - return None, None - # section - if line[0] == '[': - realline = line - for c in COMMENTCHARS: - line = line.split(c)[0].rstrip() - if line[-1] == "]": - return line[1:-1], None - return None, realline.strip() - # value - elif not line[0].isspace(): - try: - name, value = line.split('=', 1) - if ":" in name: - raise ValueError() - except ValueError: - try: - name, value = line.split(":", 1) - except ValueError: - self._raise(lineno, 'unexpected line: %r' % line) - return name.strip(), value.strip() - # continuation - else: - return None, line.strip() - - def lineof(self, section, name=None): - lineno = self._sources.get((section, name)) - if lineno is not None: - return lineno + 1 - - def get(self, section, name, default=None, convert=str): - try: - return convert(self.sections[section][name]) - except KeyError: - return default - - def __getitem__(self, name): - if name not in self.sections: - raise KeyError(name) - return SectionWrapper(self, name) - - def __iter__(self): - for name in sorted(self.sections, key=self.lineof): - yield SectionWrapper(self, name) - - def __contains__(self, arg): - return arg in self.sections - - -def iscommentline(line): - c = line.lstrip()[:1] - return c in COMMENTCHARS diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi b/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi deleted file mode 100644 index b6284bec3f6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/iniconfig/__init__.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Callable, Iterator, Mapping, Optional, Tuple, TypeVar, Union -from typing_extensions import Final - -_D = TypeVar('_D') -_T = TypeVar('_T') - -class ParseError(Exception): - # Private __init__. - path: Final[str] - lineno: Final[int] - msg: Final[str] - -class SectionWrapper: - # Private __init__. - config: Final[IniConfig] - name: Final[str] - def __getitem__(self, key: str) -> str: ... - def __iter__(self) -> Iterator[str]: ... - def get(self, key: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... - def items(self) -> Iterator[Tuple[str, str]]: ... - def lineof(self, name: str) -> Optional[int]: ... - -class IniConfig: - path: Final[str] - sections: Final[Mapping[str, Mapping[str, str]]] - def __init__(self, path: str, data: Optional[str] = None): ... - def __contains__(self, arg: str) -> bool: ... - def __getitem__(self, name: str) -> SectionWrapper: ... - def __iter__(self) -> Iterator[SectionWrapper]: ... - def get(self, section: str, name: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... - def lineof(self, section: str, name: Optional[str] = ...) -> Optional[int]: ... diff --git a/tests/wpt/tests/tools/third_party/py/py/_xmlgen.py b/tests/wpt/tests/tools/third_party/py/py/_xmlgen.py deleted file mode 100644 index 1c835458843..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/_xmlgen.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -module for generating and serializing xml and html structures -by using simple python objects. - -(c) holger krekel, holger at merlinux eu. 2009 -""" -import sys, re - -if sys.version_info >= (3,0): - def u(s): - return s - def unicode(x, errors=None): - if hasattr(x, '__unicode__'): - return x.__unicode__() - return str(x) -else: - def u(s): - return unicode(s) - unicode = unicode - - -class NamespaceMetaclass(type): - def __getattr__(self, name): - if name[:1] == '_': - raise AttributeError(name) - if self == Namespace: - raise ValueError("Namespace class is abstract") - tagspec = self.__tagspec__ - if tagspec is not None and name not in tagspec: - raise AttributeError(name) - classattr = {} - if self.__stickyname__: - classattr['xmlname'] = name - cls = type(name, (self.__tagclass__,), classattr) - setattr(self, name, cls) - return cls - -class Tag(list): - class Attr(object): - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __init__(self, *args, **kwargs): - super(Tag, self).__init__(args) - self.attr = self.Attr(**kwargs) - - def __unicode__(self): - return self.unicode(indent=0) - __str__ = __unicode__ - - def unicode(self, indent=2): - l = [] - SimpleUnicodeVisitor(l.append, indent).visit(self) - return u("").join(l) - - def __repr__(self): - name = self.__class__.__name__ - return "<%r tag object %d>" % (name, id(self)) - -Namespace = NamespaceMetaclass('Namespace', (object, ), { - '__tagspec__': None, - '__tagclass__': Tag, - '__stickyname__': False, -}) - -class HtmlTag(Tag): - def unicode(self, indent=2): - l = [] - HtmlVisitor(l.append, indent, shortempty=False).visit(self) - return u("").join(l) - -# exported plain html namespace -class html(Namespace): - __tagclass__ = HtmlTag - __stickyname__ = True - __tagspec__ = dict([(x,1) for x in ( - 'a,abbr,acronym,address,applet,area,article,aside,audio,b,' - 'base,basefont,bdi,bdo,big,blink,blockquote,body,br,button,' - 'canvas,caption,center,cite,code,col,colgroup,command,comment,' - 'datalist,dd,del,details,dfn,dir,div,dl,dt,em,embed,' - 'fieldset,figcaption,figure,footer,font,form,frame,frameset,h1,' - 'h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,img,input,' - 'ins,isindex,kbd,keygen,label,legend,li,link,listing,map,mark,' - 'marquee,menu,meta,meter,multicol,nav,nobr,noembed,noframes,' - 'noscript,object,ol,optgroup,option,output,p,param,pre,progress,' - 'q,rp,rt,ruby,s,samp,script,section,select,small,source,span,' - 'strike,strong,style,sub,summary,sup,table,tbody,td,textarea,' - 'tfoot,th,thead,time,title,tr,track,tt,u,ul,xmp,var,video,wbr' - ).split(',') if x]) - - class Style(object): - def __init__(self, **kw): - for x, y in kw.items(): - x = x.replace('_', '-') - setattr(self, x, y) - - -class raw(object): - """just a box that can contain a unicode string that will be - included directly in the output""" - def __init__(self, uniobj): - self.uniobj = uniobj - -class SimpleUnicodeVisitor(object): - """ recursive visitor to write unicode. """ - def __init__(self, write, indent=0, curindent=0, shortempty=True): - self.write = write - self.cache = {} - self.visited = {} # for detection of recursion - self.indent = indent - self.curindent = curindent - self.parents = [] - self.shortempty = shortempty # short empty tags or not - - def visit(self, node): - """ dispatcher on node's class/bases name. """ - cls = node.__class__ - try: - visitmethod = self.cache[cls] - except KeyError: - for subclass in cls.__mro__: - visitmethod = getattr(self, subclass.__name__, None) - if visitmethod is not None: - break - else: - visitmethod = self.__object - self.cache[cls] = visitmethod - visitmethod(node) - - # the default fallback handler is marked private - # to avoid clashes with the tag name object - def __object(self, obj): - #self.write(obj) - self.write(escape(unicode(obj))) - - def raw(self, obj): - self.write(obj.uniobj) - - def list(self, obj): - assert id(obj) not in self.visited - self.visited[id(obj)] = 1 - for elem in obj: - self.visit(elem) - - def Tag(self, tag): - assert id(tag) not in self.visited - try: - tag.parent = self.parents[-1] - except IndexError: - tag.parent = None - self.visited[id(tag)] = 1 - tagname = getattr(tag, 'xmlname', tag.__class__.__name__) - if self.curindent and not self._isinline(tagname): - self.write("\n" + u(' ') * self.curindent) - if tag: - self.curindent += self.indent - self.write(u('<%s%s>') % (tagname, self.attributes(tag))) - self.parents.append(tag) - for x in tag: - self.visit(x) - self.parents.pop() - self.write(u('</%s>') % tagname) - self.curindent -= self.indent - else: - nameattr = tagname+self.attributes(tag) - if self._issingleton(tagname): - self.write(u('<%s/>') % (nameattr,)) - else: - self.write(u('<%s></%s>') % (nameattr, tagname)) - - def attributes(self, tag): - # serialize attributes - attrlist = dir(tag.attr) - attrlist.sort() - l = [] - for name in attrlist: - res = self.repr_attribute(tag.attr, name) - if res is not None: - l.append(res) - l.extend(self.getstyle(tag)) - return u("").join(l) - - def repr_attribute(self, attrs, name): - if name[:2] != '__': - value = getattr(attrs, name) - if name.endswith('_'): - name = name[:-1] - if isinstance(value, raw): - insert = value.uniobj - else: - insert = escape(unicode(value)) - return ' %s="%s"' % (name, insert) - - def getstyle(self, tag): - """ return attribute list suitable for styling. """ - try: - styledict = tag.style.__dict__ - except AttributeError: - return [] - else: - stylelist = [x+': ' + y for x,y in styledict.items()] - return [u(' style="%s"') % u('; ').join(stylelist)] - - def _issingleton(self, tagname): - """can (and will) be overridden in subclasses""" - return self.shortempty - - def _isinline(self, tagname): - """can (and will) be overridden in subclasses""" - return False - -class HtmlVisitor(SimpleUnicodeVisitor): - - single = dict([(x, 1) for x in - ('br,img,area,param,col,hr,meta,link,base,' - 'input,frame').split(',')]) - inline = dict([(x, 1) for x in - ('a abbr acronym b basefont bdo big br cite code dfn em font ' - 'i img input kbd label q s samp select small span strike ' - 'strong sub sup textarea tt u var'.split(' '))]) - - def repr_attribute(self, attrs, name): - if name == 'class_': - value = getattr(attrs, name) - if value is None: - return - return super(HtmlVisitor, self).repr_attribute(attrs, name) - - def _issingleton(self, tagname): - return tagname in self.single - - def _isinline(self, tagname): - return tagname in self.inline - - -class _escape: - def __init__(self): - self.escape = { - u('"') : u('"'), u('<') : u('<'), u('>') : u('>'), - u('&') : u('&'), u("'") : u('''), - } - self.charef_rex = re.compile(u("|").join(self.escape.keys())) - - def _replacer(self, match): - return self.escape[match.group(0)] - - def __call__(self, ustring): - """ xml-escape the given unicode string. """ - try: - ustring = unicode(ustring) - except UnicodeDecodeError: - ustring = unicode(ustring, 'utf-8', errors='replace') - return self.charef_rex.sub(self._replacer, ustring) - -escape = _escape() diff --git a/tests/wpt/tests/tools/third_party/py/py/error.pyi b/tests/wpt/tests/tools/third_party/py/py/error.pyi deleted file mode 100644 index 034eba609f1..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/error.pyi +++ /dev/null @@ -1,129 +0,0 @@ -from typing import Any, Callable, TypeVar - -_T = TypeVar('_T') - -def checked_call(func: Callable[..., _T], *args: Any, **kwargs: Any) -> _T: ... -class Error(EnvironmentError): ... -class EPERM(Error): ... -class ENOENT(Error): ... -class ESRCH(Error): ... -class EINTR(Error): ... -class EIO(Error): ... -class ENXIO(Error): ... -class E2BIG(Error): ... -class ENOEXEC(Error): ... -class EBADF(Error): ... -class ECHILD(Error): ... -class EAGAIN(Error): ... -class ENOMEM(Error): ... -class EACCES(Error): ... -class EFAULT(Error): ... -class ENOTBLK(Error): ... -class EBUSY(Error): ... -class EEXIST(Error): ... -class EXDEV(Error): ... -class ENODEV(Error): ... -class ENOTDIR(Error): ... -class EISDIR(Error): ... -class EINVAL(Error): ... -class ENFILE(Error): ... -class EMFILE(Error): ... -class ENOTTY(Error): ... -class ETXTBSY(Error): ... -class EFBIG(Error): ... -class ENOSPC(Error): ... -class ESPIPE(Error): ... -class EROFS(Error): ... -class EMLINK(Error): ... -class EPIPE(Error): ... -class EDOM(Error): ... -class ERANGE(Error): ... -class EDEADLCK(Error): ... -class ENAMETOOLONG(Error): ... -class ENOLCK(Error): ... -class ENOSYS(Error): ... -class ENOTEMPTY(Error): ... -class ELOOP(Error): ... -class EWOULDBLOCK(Error): ... -class ENOMSG(Error): ... -class EIDRM(Error): ... -class ECHRNG(Error): ... -class EL2NSYNC(Error): ... -class EL3HLT(Error): ... -class EL3RST(Error): ... -class ELNRNG(Error): ... -class EUNATCH(Error): ... -class ENOCSI(Error): ... -class EL2HLT(Error): ... -class EBADE(Error): ... -class EBADR(Error): ... -class EXFULL(Error): ... -class ENOANO(Error): ... -class EBADRQC(Error): ... -class EBADSLT(Error): ... -class EDEADLOCK(Error): ... -class EBFONT(Error): ... -class ENOSTR(Error): ... -class ENODATA(Error): ... -class ETIME(Error): ... -class ENOSR(Error): ... -class ENONET(Error): ... -class ENOPKG(Error): ... -class EREMOTE(Error): ... -class ENOLINK(Error): ... -class EADV(Error): ... -class ESRMNT(Error): ... -class ECOMM(Error): ... -class EPROTO(Error): ... -class EMULTIHOP(Error): ... -class EDOTDOT(Error): ... -class EBADMSG(Error): ... -class EOVERFLOW(Error): ... -class ENOTUNIQ(Error): ... -class EBADFD(Error): ... -class EREMCHG(Error): ... -class ELIBACC(Error): ... -class ELIBBAD(Error): ... -class ELIBSCN(Error): ... -class ELIBMAX(Error): ... -class ELIBEXEC(Error): ... -class EILSEQ(Error): ... -class ERESTART(Error): ... -class ESTRPIPE(Error): ... -class EUSERS(Error): ... -class ENOTSOCK(Error): ... -class EDESTADDRREQ(Error): ... -class EMSGSIZE(Error): ... -class EPROTOTYPE(Error): ... -class ENOPROTOOPT(Error): ... -class EPROTONOSUPPORT(Error): ... -class ESOCKTNOSUPPORT(Error): ... -class ENOTSUP(Error): ... -class EOPNOTSUPP(Error): ... -class EPFNOSUPPORT(Error): ... -class EAFNOSUPPORT(Error): ... -class EADDRINUSE(Error): ... -class EADDRNOTAVAIL(Error): ... -class ENETDOWN(Error): ... -class ENETUNREACH(Error): ... -class ENETRESET(Error): ... -class ECONNABORTED(Error): ... -class ECONNRESET(Error): ... -class ENOBUFS(Error): ... -class EISCONN(Error): ... -class ENOTCONN(Error): ... -class ESHUTDOWN(Error): ... -class ETOOMANYREFS(Error): ... -class ETIMEDOUT(Error): ... -class ECONNREFUSED(Error): ... -class EHOSTDOWN(Error): ... -class EHOSTUNREACH(Error): ... -class EALREADY(Error): ... -class EINPROGRESS(Error): ... -class ESTALE(Error): ... -class EUCLEAN(Error): ... -class ENOTNAM(Error): ... -class ENAVAIL(Error): ... -class EISNAM(Error): ... -class EREMOTEIO(Error): ... -class EDQUOT(Error): ... diff --git a/tests/wpt/tests/tools/third_party/py/py/iniconfig.pyi b/tests/wpt/tests/tools/third_party/py/py/iniconfig.pyi deleted file mode 100644 index b6284bec3f6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/iniconfig.pyi +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Callable, Iterator, Mapping, Optional, Tuple, TypeVar, Union -from typing_extensions import Final - -_D = TypeVar('_D') -_T = TypeVar('_T') - -class ParseError(Exception): - # Private __init__. - path: Final[str] - lineno: Final[int] - msg: Final[str] - -class SectionWrapper: - # Private __init__. - config: Final[IniConfig] - name: Final[str] - def __getitem__(self, key: str) -> str: ... - def __iter__(self) -> Iterator[str]: ... - def get(self, key: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... - def items(self) -> Iterator[Tuple[str, str]]: ... - def lineof(self, name: str) -> Optional[int]: ... - -class IniConfig: - path: Final[str] - sections: Final[Mapping[str, Mapping[str, str]]] - def __init__(self, path: str, data: Optional[str] = None): ... - def __contains__(self, arg: str) -> bool: ... - def __getitem__(self, name: str) -> SectionWrapper: ... - def __iter__(self) -> Iterator[SectionWrapper]: ... - def get(self, section: str, name: str, default: _D = ..., convert: Callable[[str], _T] = ...) -> Union[_T, _D]: ... - def lineof(self, section: str, name: Optional[str] = ...) -> Optional[int]: ... diff --git a/tests/wpt/tests/tools/third_party/py/py/io.pyi b/tests/wpt/tests/tools/third_party/py/py/io.pyi deleted file mode 100644 index d377e2405d5..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/io.pyi +++ /dev/null @@ -1,130 +0,0 @@ -from io import StringIO as TextIO -from io import BytesIO as BytesIO -from typing import Any, AnyStr, Callable, Generic, IO, List, Optional, Text, Tuple, TypeVar, Union, overload -from typing_extensions import Final -import sys - -_T = TypeVar("_T") - -class FDCapture(Generic[AnyStr]): - def __init__(self, targetfd: int, tmpfile: Optional[IO[AnyStr]] = ..., now: bool = ..., patchsys: bool = ...) -> None: ... - def start(self) -> None: ... - def done(self) -> IO[AnyStr]: ... - def writeorg(self, data: AnyStr) -> None: ... - -class StdCaptureFD: - def __init__( - self, - out: Union[bool, IO[str]] = ..., - err: Union[bool, IO[str]] = ..., - mixed: bool = ..., - in_: bool = ..., - patchsys: bool = ..., - now: bool = ..., - ) -> None: ... - @classmethod - def call(cls, func: Callable[..., _T], *args: Any, **kwargs: Any) -> Tuple[_T, str, str]: ... - def reset(self) -> Tuple[str, str]: ... - def suspend(self) -> Tuple[str, str]: ... - def startall(self) -> None: ... - def resume(self) -> None: ... - def done(self, save: bool = ...) -> Tuple[IO[str], IO[str]]: ... - def readouterr(self) -> Tuple[str, str]: ... - -class StdCapture: - def __init__( - self, - out: Union[bool, IO[str]] = ..., - err: Union[bool, IO[str]] = ..., - in_: bool = ..., - mixed: bool = ..., - now: bool = ..., - ) -> None: ... - @classmethod - def call(cls, func: Callable[..., _T], *args: Any, **kwargs: Any) -> Tuple[_T, str, str]: ... - def reset(self) -> Tuple[str, str]: ... - def suspend(self) -> Tuple[str, str]: ... - def startall(self) -> None: ... - def resume(self) -> None: ... - def done(self, save: bool = ...) -> Tuple[IO[str], IO[str]]: ... - def readouterr(self) -> Tuple[IO[str], IO[str]]: ... - -# XXX: The type here is not exactly right. If f is IO[bytes] and -# encoding is not None, returns some weird hybrid, not exactly IO[bytes]. -def dupfile( - f: IO[AnyStr], - mode: Optional[str] = ..., - buffering: int = ..., - raising: bool = ..., - encoding: Optional[str] = ..., -) -> IO[AnyStr]: ... -def get_terminal_width() -> int: ... -def ansi_print( - text: Union[str, Text], - esc: Union[Union[str, Text], Tuple[Union[str, Text], ...]], - file: Optional[IO[Any]] = ..., - newline: bool = ..., - flush: bool = ..., -) -> None: ... -def saferepr(obj, maxsize: int = ...) -> str: ... - -class TerminalWriter: - stringio: TextIO - encoding: Final[str] - hasmarkup: bool - def __init__(self, file: Optional[IO[str]] = ..., stringio: bool = ..., encoding: Optional[str] = ...) -> None: ... - @property - def fullwidth(self) -> int: ... - @fullwidth.setter - def fullwidth(self, value: int) -> None: ... - @property - def chars_on_current_line(self) -> int: ... - @property - def width_of_current_line(self) -> int: ... - def markup( - self, - text: str, - *, - black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., - cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., - Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., - blink: int = ..., invert: int = ..., - ) -> str: ... - def sep( - self, - sepchar: str, - title: Optional[str] = ..., - fullwidth: Optional[int] = ..., - *, - black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., - cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., - Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., - blink: int = ..., invert: int = ..., - ) -> None: ... - def write( - self, - msg: str, - *, - black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., - cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., - Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., - blink: int = ..., invert: int = ..., - ) -> None: ... - def line( - self, - s: str = ..., - *, - black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., - cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., - Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., - blink: int = ..., invert: int = ..., - ) -> None: ... - def reline( - self, - line: str, - *, - black: int = ..., red: int = ..., green: int = ..., yellow: int = ..., blue: int = ..., purple: int = ..., - cyan: int = ..., white: int = ..., Black: int = ..., Red: int = ..., Green: int = ..., Yellow: int = ..., - Blue: int = ..., Purple: int = ..., Cyan: int = ..., White: int = ..., bold: int = ..., light: int = ..., - blink: int = ..., invert: int = ..., - ) -> None: ... diff --git a/tests/wpt/tests/tools/third_party/py/py/path.pyi b/tests/wpt/tests/tools/third_party/py/py/path.pyi deleted file mode 100644 index 1ddab9601ea..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/path.pyi +++ /dev/null @@ -1,197 +0,0 @@ -from typing import Any, AnyStr, Callable, ContextManager, Generic, IO, Iterable, Iterator, List, Optional, Text, Type, Union -from typing_extensions import Final, Literal -import os -import sys - -class _FNMatcher(Generic[AnyStr]): - pattern: AnyStr = ... - def __init__(self, pattern: AnyStr) -> None: ... - def __call__(self, path: local) -> bool: ... - -class _Stat: - path: Final[local] = ... - mode: Final[int] - ino: Final[int] - dev: Final[int] - nlink: Final[int] - uid: Final[int] - gid: Final[int] - size: Final[int] - atime: Final[float] - mtime: Final[float] - ctime: Final[float] - atime_ns: Final[int] - mtime_ns: Final[int] - ctime_ns: Final[int] - if sys.version_info >= (3, 8) and sys.platform == "win32": - reparse_tag: Final[int] - blocks: Final[int] - blksize: Final[int] - rdev: Final[int] - flags: Final[int] - gen: Final[int] - birthtime: Final[int] - rsize: Final[int] - creator: Final[int] - type: Final[int] - if sys.platform != 'win32': - @property - def owner(self) -> str: ... - @property - def group(self) -> str: ... - def isdir(self) -> bool: ... - def isfile(self) -> bool: ... - def islink(self) -> bool: ... - - -if sys.version_info >= (3, 6): - _PathLike = os.PathLike -else: - class _PathLike(Generic[AnyStr]): - def __fspath__(self) -> AnyStr: ... -_PathType = Union[bytes, Text, _PathLike[str], _PathLike[bytes], local] - -class local(_PathLike[str]): - class ImportMismatchError(ImportError): ... - - sep: Final[str] - strpath: Final[str] - - def __init__(self, path: _PathType = ..., expanduser: bool = ...) -> None: ... - def __hash__(self) -> int: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - def __lt__(self, other: object) -> bool: ... - def __gt__(self, other: object) -> bool: ... - def __add__(self, other: object) -> local: ... - def __cmp__(self, other: object) -> int: ... - def __div__(self, other: _PathType) -> local: ... - def __truediv__(self, other: _PathType) -> local: ... - def __fspath__(self) -> str: ... - - @classmethod - def get_temproot(cls) -> local: ... - @classmethod - def make_numbered_dir( - cls, - prefix: str = ..., - rootdir: Optional[local] = ..., - keep: Optional[int] = ..., - lock_timeout: int = ..., - ) -> local: ... - @classmethod - def mkdtemp(cls, rootdir: Optional[local] = ...) -> local: ... - @classmethod - def sysfind( - cls, - name: _PathType, - checker: Optional[Callable[[local], bool]] = ..., - paths: Optional[Iterable[_PathType]] = ..., - ) -> Optional[local]: ... - - @property - def basename(self) -> str: ... - @property - def dirname(self) -> str: ... - @property - def purebasename(self) -> str: ... - @property - def ext(self) -> str: ... - - def as_cwd(self) -> ContextManager[Optional[local]]: ... - def atime(self) -> float: ... - def bestrelpath(self, dest: local) -> str: ... - def chdir(self) -> local: ... - def check( - self, - *, - basename: int = ..., notbasename: int = ..., - basestarts: int = ..., notbasestarts: int = ..., - dir: int = ..., notdir: int = ..., - dotfile: int = ..., notdotfile: int = ..., - endswith: int = ..., notendswith: int = ..., - exists: int = ..., notexists: int = ..., - ext: int = ..., notext: int = ..., - file: int = ..., notfile: int = ..., - fnmatch: int = ..., notfnmatch: int = ..., - link: int = ..., notlink: int = ..., - relto: int = ..., notrelto: int = ..., - ) -> bool: ... - def chmod(self, mode: int, rec: Union[int, str, Text, Callable[[local], bool]] = ...) -> None: ... - if sys.platform != 'win32': - def chown(self, user: Union[int, str], group: Union[int, str], rec: int = ...) -> None: ... - def common(self, other: local) -> Optional[local]: ... - def computehash(self, hashtype: str = ..., chunksize: int = ...) -> str: ... - def copy(self, target: local, mode: bool = ..., stat: bool = ...) -> None: ... - def dirpath(self, *args: _PathType, abs: int = ...) -> local: ... - def dump(self, obj: Any, bin: Optional[int] = ...) -> None: ... - def ensure(self, *args: _PathType, dir: int = ...) -> local: ... - def ensure_dir(self, *args: _PathType) -> local: ... - def exists(self) -> bool: ... - def fnmatch(self, pattern: str): _FNMatcher - def isdir(self) -> bool: ... - def isfile(self) -> bool: ... - def islink(self) -> bool: ... - def join(self, *args: _PathType, abs: int = ...) -> local: ... - def listdir( - self, - fil: Optional[Union[str, Text, Callable[[local], bool]]] = ..., - sort: Optional[bool] = ..., - ) -> List[local]: ... - def load(self) -> Any: ... - def lstat(self) -> _Stat: ... - def mkdir(self, *args: _PathType) -> local: ... - if sys.platform != 'win32': - def mklinkto(self, oldname: Union[str, local]) -> None: ... - def mksymlinkto(self, value: local, absolute: int = ...) -> None: ... - def move(self, target: local) -> None: ... - def mtime(self) -> float: ... - def new( - self, - *, - drive: str = ..., - dirname: str = ..., - basename: str = ..., - purebasename: str = ..., - ext: str = ..., - ) -> local: ... - def open(self, mode: str = ..., ensure: bool = ..., encoding: Optional[str] = ...) -> IO[Any]: ... - def parts(self, reverse: bool = ...) -> List[local]: ... - def pyimport( - self, - modname: Optional[str] = ..., - ensuresyspath: Union[bool, Literal["append", "importlib"]] = ..., - ) -> Any: ... - def pypkgpath(self) -> Optional[local]: ... - def read(self, mode: str = ...) -> Union[Text, bytes]: ... - def read_binary(self) -> bytes: ... - def read_text(self, encoding: str) -> Text: ... - def readlines(self, cr: int = ...) -> List[str]: ... - if sys.platform != 'win32': - def readlink(self) -> str: ... - def realpath(self) -> local: ... - def relto(self, relpath: Union[str, local]) -> str: ... - def remove(self, rec: int = ..., ignore_errors: bool = ...) -> None: ... - def rename(self, target: _PathType) -> None: ... - def samefile(self, other: _PathType) -> bool: ... - def setmtime(self, mtime: Optional[float] = ...) -> None: ... - def size(self) -> int: ... - def stat(self, raising: bool = ...) -> _Stat: ... - def sysexec(self, *argv: Any, **popen_opts: Any) -> Text: ... - def visit( - self, - fil: Optional[Union[str, Text, Callable[[local], bool]]] = ..., - rec: Optional[Union[Literal[1, True], str, Text, Callable[[local], bool]]] = ..., - ignore: Type[Exception] = ..., - bf: bool = ..., - sort: bool = ..., - ) -> Iterator[local]: ... - def write(self, data: Any, mode: str = ..., ensure: bool = ...) -> None: ... - def write_binary(self, data: bytes, ensure: bool = ...) -> None: ... - def write_text(self, data: Union[str, Text], encoding: str, ensure: bool = ...) -> None: ... - - -# Untyped types below here. -svnwc: Any -svnurl: Any -SvnAuth: Any diff --git a/tests/wpt/tests/tools/third_party/py/py/test.py b/tests/wpt/tests/tools/third_party/py/py/test.py deleted file mode 100644 index aa5beb1789f..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/test.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys -if __name__ == '__main__': - import pytest - sys.exit(pytest.main()) -else: - import sys, pytest - sys.modules['py.test'] = pytest - -# for more API entry points see the 'tests' definition -# in __init__.py diff --git a/tests/wpt/tests/tools/third_party/py/py/xml.pyi b/tests/wpt/tests/tools/third_party/py/py/xml.pyi deleted file mode 100644 index 9c44480a5f3..00000000000 --- a/tests/wpt/tests/tools/third_party/py/py/xml.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from typing import ClassVar, Generic, Iterable, Text, Type, Union -from typing_extensions import Final - -class raw: - uniobj: Final[Text] - def __init__(self, uniobj: Text) -> None: ... - -class _NamespaceMetaclass(type): - def __getattr__(self, name: str) -> Type[Tag]: ... - -class Namespace(metaclass=_NamespaceMetaclass): ... - -class Tag(list): - class Attr: - def __getattr__(self, attr: str) -> Text: ... - attr: Final[Attr] - def __init__(self, *args: Union[Text, raw, Tag, Iterable[Tag]], **kwargs: Union[Text, raw]) -> None: ... - def unicode(self, indent: int = ...) -> Text: ... - -class html(Namespace): - class Style: - def __init__(self, **kw: Union[str, Text]) -> None: ... - style: ClassVar[Style] - -def escape(ustring: Union[str, Text]) -> Text: ... diff --git a/tests/wpt/tests/tools/third_party/py/pyproject.toml b/tests/wpt/tests/tools/third_party/py/pyproject.toml deleted file mode 100644 index e386ea0b271..00000000000 --- a/tests/wpt/tests/tools/third_party/py/pyproject.toml +++ /dev/null @@ -1,6 +0,0 @@ -[build-system] -requires = [ - "setuptools", - "setuptools_scm[toml]", -] -build-backend = "setuptools.build_meta" diff --git a/tests/wpt/tests/tools/third_party/py/setup.cfg b/tests/wpt/tests/tools/third_party/py/setup.cfg deleted file mode 100644 index 5f25c2febfd..00000000000 --- a/tests/wpt/tests/tools/third_party/py/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[wheel] -universal = 1 - -[metadata] -license_file = LICENSE - -[devpi:upload] -formats=sdist.tgz,bdist_wheel diff --git a/tests/wpt/tests/tools/third_party/py/setup.py b/tests/wpt/tests/tools/third_party/py/setup.py deleted file mode 100644 index 5948ef0047a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -from setuptools import setup, find_packages - - -def main(): - setup( - name='py', - description='library with cross-python path, ini-parsing, io, code, log facilities', - long_description=open('README.rst').read(), - use_scm_version={"write_to": "py/_version.py"}, - setup_requires=["setuptools_scm"], - url='https://py.readthedocs.io/', - license='MIT license', - platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', - author='holger krekel, Ronny Pfannschmidt, Benjamin Peterson and others', - author_email='pytest-dev@python.org', - classifiers=['Development Status :: 6 - Mature', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: MacOS :: MacOS X', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Libraries', - 'Topic :: Utilities', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - ], - packages=find_packages(exclude=['tasks', 'testing']), - include_package_data=True, - zip_safe=False, - package_data={ - "": ["py.typed"], - }, - ) - -if __name__ == '__main__': - main() diff --git a/tests/wpt/tests/tools/third_party/py/tasks/vendoring.py b/tests/wpt/tests/tools/third_party/py/tasks/vendoring.py deleted file mode 100644 index 3c7d6015cfa..00000000000 --- a/tests/wpt/tests/tools/third_party/py/tasks/vendoring.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import absolute_import, print_function -import os.path -import shutil -import subprocess -import sys - -VENDOR_TARGET = "py/_vendored_packages" -GOOD_FILES = ('README.md', '__init__.py') - - -def remove_libs(): - print("removing vendored libs") - for filename in os.listdir(VENDOR_TARGET): - if filename not in GOOD_FILES: - path = os.path.join(VENDOR_TARGET, filename) - print(" ", path) - if os.path.isfile(path): - os.remove(path) - else: - shutil.rmtree(path) - - -def update_libs(): - print("installing libs") - subprocess.check_call(( - sys.executable, '-m', 'pip', 'install', - '--target', VENDOR_TARGET, 'apipkg', 'iniconfig', - )) - subprocess.check_call(('git', 'add', VENDOR_TARGET)) - print("Please commit to finish the update after running the tests:") - print() - print(' git commit -am "Updated vendored libs"') - - -def main(): - remove_libs() - update_libs() - - -if __name__ == '__main__': - exit(main()) diff --git a/tests/wpt/tests/tools/third_party/py/testing/code/test_assertion.py b/tests/wpt/tests/tools/third_party/py/testing/code/test_assertion.py deleted file mode 100644 index e2a7f903998..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/code/test_assertion.py +++ /dev/null @@ -1,305 +0,0 @@ -import pytest, py -import re - -def exvalue(): - import sys - return sys.exc_info()[1] - -def f(): - return 2 - -def test_assert(): - try: - assert f() == 3 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 == 3\n') - - -def test_assert_within_finally(): - excinfo = py.test.raises(ZeroDivisionError, """ - try: - 1/0 - finally: - i = 42 - """) - s = excinfo.exconly() - assert re.search("ZeroDivisionError:.*division", s) is not None - - -def test_assert_multiline_1(): - try: - assert (f() == - 3) - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 == 3\n') - -def test_assert_multiline_2(): - try: - assert (f() == (4, - 3)[-1]) - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith('assert 2 ==') - -def test_in(): - try: - assert "hi" in [1, 2] - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 'hi' in") - -def test_is(): - try: - assert 1 is 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 is 2") - - -def test_attrib(): - class Foo(object): - b = 1 - i = Foo() - try: - assert i.b == 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 == 2") - -def test_attrib_inst(): - class Foo(object): - b = 1 - try: - assert Foo().b == 2 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 1 == 2") - -def test_len(): - l = list(range(42)) - try: - assert len(l) == 100 - except AssertionError: - e = exvalue() - s = str(e) - assert s.startswith("assert 42 == 100") - assert "where 42 = len([" in s - - -def test_assert_keyword_arg(): - def f(x=3): - return False - try: - assert f(x=5) - except AssertionError: - e = exvalue() - assert "x=5" in str(e) - -# These tests should both fail, but should fail nicely... -class WeirdRepr: - def __repr__(self): - return '<WeirdRepr\nsecond line>' - -def bug_test_assert_repr(): - v = WeirdRepr() - try: - assert v == 1 - except AssertionError: - e = exvalue() - assert str(e).find('WeirdRepr') != -1 - assert str(e).find('second line') != -1 - assert 0 - -def test_assert_non_string(): - try: - assert 0, ['list'] - except AssertionError: - e = exvalue() - assert str(e).find("list") != -1 - -def test_assert_implicit_multiline(): - try: - x = [1,2,3] - assert x != [1, - 2, 3] - except AssertionError: - e = exvalue() - assert str(e).find('assert [1, 2, 3] !=') != -1 - -@py.test.mark.xfail(py.test.__version__[0] != "2", - reason="broken on modern pytest", - run=False -) -def test_assert_with_brokenrepr_arg(): - class BrokenRepr: - def __repr__(self): 0 / 0 - e = AssertionError(BrokenRepr()) - if e.msg.find("broken __repr__") == -1: - py.test.fail("broken __repr__ not handle correctly") - -def test_multiple_statements_per_line(): - try: - a = 1; assert a == 2 - except AssertionError: - e = exvalue() - assert "assert 1 == 2" in str(e) - -def test_power(): - try: - assert 2**3 == 7 - except AssertionError: - e = exvalue() - assert "assert (2 ** 3) == 7" in str(e) - - -class TestView: - - def setup_class(cls): - cls.View = py.test.importorskip("py._code._assertionold").View - - def test_class_dispatch(self): - ### Use a custom class hierarchy with existing instances - - class Picklable(self.View): - pass - - class Simple(Picklable): - __view__ = object - def pickle(self): - return repr(self.__obj__) - - class Seq(Picklable): - __view__ = list, tuple, dict - def pickle(self): - return ';'.join( - [Picklable(item).pickle() for item in self.__obj__]) - - class Dict(Seq): - __view__ = dict - def pickle(self): - return Seq.pickle(self) + '!' + Seq(self.values()).pickle() - - assert Picklable(123).pickle() == '123' - assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' - assert Picklable({1:2}).pickle() == '1!2' - - def test_viewtype_class_hierarchy(self): - # Use a custom class hierarchy based on attributes of existing instances - class Operation: - "Existing class that I don't want to change." - def __init__(self, opname, *args): - self.opname = opname - self.args = args - - existing = [Operation('+', 4, 5), - Operation('getitem', '', 'join'), - Operation('setattr', 'x', 'y', 3), - Operation('-', 12, 1)] - - class PyOp(self.View): - def __viewkey__(self): - return self.opname - def generate(self): - return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) - - class PyBinaryOp(PyOp): - __view__ = ('+', '-', '*', '/') - def generate(self): - return '%s %s %s' % (self.args[0], self.opname, self.args[1]) - - codelines = [PyOp(op).generate() for op in existing] - assert codelines == ["4 + 5", "getitem('', 'join')", - "setattr('x', 'y', 3)", "12 - 1"] - -def test_underscore_api(): - py.code._AssertionError - py.code._reinterpret_old # used by pypy - py.code._reinterpret - -def test_assert_customizable_reprcompare(monkeypatch): - util = pytest.importorskip("_pytest.assertion.util") - monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello') - try: - assert 3 == 4 - except AssertionError: - e = exvalue() - s = str(e) - assert "hello" in s - -def test_assert_long_source_1(): - try: - assert len == [ - (None, ['somet text', 'more text']), - ] - except AssertionError: - e = exvalue() - s = str(e) - assert 're-run' not in s - assert 'somet text' in s - -def test_assert_long_source_2(): - try: - assert(len == [ - (None, ['somet text', 'more text']), - ]) - except AssertionError: - e = exvalue() - s = str(e) - assert 're-run' not in s - assert 'somet text' in s - -def test_assert_raise_alias(testdir): - testdir.makepyfile(""" - import sys - EX = AssertionError - def test_hello(): - raise EX("hello" - "multi" - "line") - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*def test_hello*", - "*raise EX*", - "*1 failed*", - ]) - -@py.test.mark.xfail(py.test.__version__[0] != "2", - reason="broken on modern pytest", - run=False) -def test_assert_raise_subclass(): - class SomeEx(AssertionError): - def __init__(self, *args): - super(SomeEx, self).__init__() - try: - raise SomeEx("hello") - except AssertionError as e: - s = str(e) - assert 're-run' not in s - assert 'could not determine' in s - -def test_assert_raises_in_nonzero_of_object_pytest_issue10(): - class A(object): - def __nonzero__(self): - raise ValueError(42) - def __lt__(self, other): - return A() - def __repr__(self): - return "<MY42 object>" - def myany(x): - return True - try: - assert not(myany(A() < 0)) - except AssertionError: - e = exvalue() - s = str(e) - assert "<MY42 object> < 0" in s diff --git a/tests/wpt/tests/tools/third_party/py/testing/code/test_code.py b/tests/wpt/tests/tools/third_party/py/testing/code/test_code.py deleted file mode 100644 index 28ec628b00d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/code/test_code.py +++ /dev/null @@ -1,159 +0,0 @@ -import py -import sys - -def test_ne(): - code1 = py.code.Code(compile('foo = "bar"', '', 'exec')) - assert code1 == code1 - code2 = py.code.Code(compile('foo = "baz"', '', 'exec')) - assert code2 != code1 - -def test_code_gives_back_name_for_not_existing_file(): - name = 'abc-123' - co_code = compile("pass\n", name, 'exec') - assert co_code.co_filename == name - code = py.code.Code(co_code) - assert str(code.path) == name - assert code.fullsource is None - -def test_code_with_class(): - class A: - pass - py.test.raises(TypeError, "py.code.Code(A)") - -if True: - def x(): - pass - -def test_code_fullsource(): - code = py.code.Code(x) - full = code.fullsource - assert 'test_code_fullsource()' in str(full) - -def test_code_source(): - code = py.code.Code(x) - src = code.source() - expected = """def x(): - pass""" - assert str(src) == expected - -def test_frame_getsourcelineno_myself(): - def func(): - return sys._getframe(0) - f = func() - f = py.code.Frame(f) - source, lineno = f.code.fullsource, f.lineno - assert source[lineno].startswith(" return sys._getframe(0)") - -def test_getstatement_empty_fullsource(): - def func(): - return sys._getframe(0) - f = func() - f = py.code.Frame(f) - prop = f.code.__class__.fullsource - try: - f.code.__class__.fullsource = None - assert f.statement == py.code.Source("") - finally: - f.code.__class__.fullsource = prop - -def test_code_from_func(): - co = py.code.Code(test_frame_getsourcelineno_myself) - assert co.firstlineno - assert co.path - - - -def test_builtin_patch_unpatch(monkeypatch): - cpy_builtin = py.builtin.builtins - comp = cpy_builtin.compile - def mycompile(*args, **kwargs): - return comp(*args, **kwargs) - class Sub(AssertionError): - pass - monkeypatch.setattr(cpy_builtin, 'AssertionError', Sub) - monkeypatch.setattr(cpy_builtin, 'compile', mycompile) - py.code.patch_builtins() - assert cpy_builtin.AssertionError != Sub - assert cpy_builtin.compile != mycompile - py.code.unpatch_builtins() - assert cpy_builtin.AssertionError is Sub - assert cpy_builtin.compile == mycompile - - -def test_unicode_handling(): - value = py.builtin._totext('\xc4\x85\xc4\x87\n', 'utf-8').encode('utf8') - def f(): - raise Exception(value) - excinfo = py.test.raises(Exception, f) - s = str(excinfo) - if sys.version_info[0] < 3: - u = unicode(excinfo) - -def test_code_getargs(): - def f1(x): - pass - c1 = py.code.Code(f1) - assert c1.getargs(var=True) == ('x',) - - def f2(x, *y): - pass - c2 = py.code.Code(f2) - assert c2.getargs(var=True) == ('x', 'y') - - def f3(x, **z): - pass - c3 = py.code.Code(f3) - assert c3.getargs(var=True) == ('x', 'z') - - def f4(x, *y, **z): - pass - c4 = py.code.Code(f4) - assert c4.getargs(var=True) == ('x', 'y', 'z') - - -def test_frame_getargs(): - def f1(x): - return sys._getframe(0) - fr1 = py.code.Frame(f1('a')) - assert fr1.getargs(var=True) == [('x', 'a')] - - def f2(x, *y): - return sys._getframe(0) - fr2 = py.code.Frame(f2('a', 'b', 'c')) - assert fr2.getargs(var=True) == [('x', 'a'), ('y', ('b', 'c'))] - - def f3(x, **z): - return sys._getframe(0) - fr3 = py.code.Frame(f3('a', b='c')) - assert fr3.getargs(var=True) == [('x', 'a'), ('z', {'b': 'c'})] - - def f4(x, *y, **z): - return sys._getframe(0) - fr4 = py.code.Frame(f4('a', 'b', c='d')) - assert fr4.getargs(var=True) == [('x', 'a'), ('y', ('b',)), - ('z', {'c': 'd'})] - - -class TestExceptionInfo: - - def test_bad_getsource(self): - try: - if False: pass - else: assert False - except AssertionError: - exci = py.code.ExceptionInfo() - assert exci.getrepr() - - -class TestTracebackEntry: - - def test_getsource(self): - try: - if False: pass - else: assert False - except AssertionError: - exci = py.code.ExceptionInfo() - entry = exci.traceback[0] - source = entry.getsource() - assert len(source) == 4 - assert 'else: assert False' in source[3] diff --git a/tests/wpt/tests/tools/third_party/py/testing/code/test_excinfo.py b/tests/wpt/tests/tools/third_party/py/testing/code/test_excinfo.py deleted file mode 100644 index c148ab8cfbd..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/code/test_excinfo.py +++ /dev/null @@ -1,956 +0,0 @@ -# -*- coding: utf-8 -*- - -import py -import pytest -import sys -from test_source import astonly - -from py._code.code import FormattedExcinfo, ReprExceptionInfo -queue = py.builtin._tryimport('queue', 'Queue') - -failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") - -try: - import importlib -except ImportError: - invalidate_import_caches = None -else: - invalidate_import_caches = getattr(importlib, "invalidate_caches", None) - - -pytest_version_info = tuple(map(int, pytest.__version__.split(".")[:3])) - -broken_on_modern_pytest = pytest.mark.xfail( - pytest_version_info[0] != 2, - reason="this test hasn't been fixed after moving py.code into pytest", - run=False - ) - - -class TWMock: - def __init__(self): - self.lines = [] - - def sep(self, sep, line=None): - self.lines.append((sep, line)) - - def line(self, line, **kw): - self.lines.append(line) - - def markup(self, text, **kw): - return text - - fullwidth = 80 - - -def test_excinfo_simple(): - try: - raise ValueError - except ValueError: - info = py.code.ExceptionInfo() - assert info.type == ValueError - - -def test_excinfo_getstatement(): - def g(): - raise ValueError - - def f(): - g() - try: - f() - except ValueError: - excinfo = py.code.ExceptionInfo() - linenumbers = [ - py.code.getrawcode(f).co_firstlineno-1+3, - py.code.getrawcode(f).co_firstlineno-1+1, - py.code.getrawcode(g).co_firstlineno-1+1, - ] - l = list(excinfo.traceback) - foundlinenumbers = [x.lineno for x in l] - assert foundlinenumbers == linenumbers - #for x in info: - # print "%s:%d %s" %(x.path.relto(root), x.lineno, x.statement) - #xxx - -# testchain for getentries test below -def f(): - # - raise ValueError - # -def g(): - # - __tracebackhide__ = True - f() - # -def h(): - # - g() - # - -class TestTraceback_f_g_h: - def setup_method(self, method): - try: - h() - except ValueError: - self.excinfo = py.code.ExceptionInfo() - - def test_traceback_entries(self): - tb = self.excinfo.traceback - entries = list(tb) - assert len(tb) == 4 # maybe fragile test - assert len(entries) == 4 # maybe fragile test - names = ['f', 'g', 'h'] - for entry in entries: - try: - names.remove(entry.frame.code.name) - except ValueError: - pass - assert not names - - def test_traceback_entry_getsource(self): - tb = self.excinfo.traceback - s = str(tb[-1].getsource()) - assert s.startswith("def f():") - assert s.endswith("raise ValueError") - - @astonly - @failsonjython - def test_traceback_entry_getsource_in_construct(self): - source = py.code.Source("""\ - def xyz(): - try: - raise ValueError - except somenoname: - pass - xyz() - """) - try: - exec (source.compile()) - except NameError: - tb = py.code.ExceptionInfo().traceback - print (tb[-1].getsource()) - s = str(tb[-1].getsource()) - assert s.startswith("def xyz():\n try:") - assert s.strip().endswith("except somenoname:") - - def test_traceback_cut(self): - co = py.code.Code(f) - path, firstlineno = co.path, co.firstlineno - traceback = self.excinfo.traceback - newtraceback = traceback.cut(path=path, firstlineno=firstlineno) - assert len(newtraceback) == 1 - newtraceback = traceback.cut(path=path, lineno=firstlineno+2) - assert len(newtraceback) == 1 - - def test_traceback_cut_excludepath(self, testdir): - p = testdir.makepyfile("def f(): raise ValueError") - excinfo = py.test.raises(ValueError, "p.pyimport().f()") - basedir = py.path.local(py.test.__file__).dirpath() - newtraceback = excinfo.traceback.cut(excludepath=basedir) - for x in newtraceback: - if hasattr(x, 'path'): - assert not py.path.local(x.path).relto(basedir) - assert newtraceback[-1].frame.code.path == p - - def test_traceback_filter(self): - traceback = self.excinfo.traceback - ntraceback = traceback.filter() - assert len(ntraceback) == len(traceback) - 1 - - def test_traceback_recursion_index(self): - def f(n): - if n < 10: - n += 1 - f(n) - excinfo = py.test.raises(RuntimeError, f, 8) - traceback = excinfo.traceback - recindex = traceback.recursionindex() - assert recindex == 3 - - def test_traceback_only_specific_recursion_errors(self, monkeypatch): - def f(n): - if n == 0: - raise RuntimeError("hello") - f(n-1) - - excinfo = pytest.raises(RuntimeError, f, 100) - monkeypatch.delattr(excinfo.traceback.__class__, "recursionindex") - repr = excinfo.getrepr() - assert "RuntimeError: hello" in str(repr.reprcrash) - - def test_traceback_no_recursion_index(self): - def do_stuff(): - raise RuntimeError - - def reraise_me(): - import sys - exc, val, tb = sys.exc_info() - py.builtin._reraise(exc, val, tb) - - def f(n): - try: - do_stuff() - except: - reraise_me() - excinfo = py.test.raises(RuntimeError, f, 8) - traceback = excinfo.traceback - recindex = traceback.recursionindex() - assert recindex is None - - def test_traceback_messy_recursion(self): - # XXX: simplified locally testable version - decorator = py.test.importorskip('decorator').decorator - - def log(f, *k, **kw): - print('%s %s' % (k, kw)) - f(*k, **kw) - log = decorator(log) - - def fail(): - raise ValueError('') - - fail = log(log(fail)) - - excinfo = py.test.raises(ValueError, fail) - assert excinfo.traceback.recursionindex() is None - - def test_traceback_getcrashentry(self): - def i(): - __tracebackhide__ = True - raise ValueError - - def h(): - i() - - def g(): - __tracebackhide__ = True - h() - - def f(): - g() - - excinfo = py.test.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry() - co = py.code.Code(h) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 1 - assert entry.frame.code.name == 'h' - - def test_traceback_getcrashentry_empty(self): - def g(): - __tracebackhide__ = True - raise ValueError - - def f(): - __tracebackhide__ = True - g() - - excinfo = py.test.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry() - co = py.code.Code(g) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 2 - assert entry.frame.code.name == 'g' - - -def hello(x): - x + 5 - - -def test_tbentry_reinterpret(): - try: - hello("hello") - except TypeError: - excinfo = py.code.ExceptionInfo() - tbentry = excinfo.traceback[-1] - msg = tbentry.reinterpret() - assert msg.startswith("TypeError: ('hello' + 5)") - - -def test_excinfo_exconly(): - excinfo = py.test.raises(ValueError, h) - assert excinfo.exconly().startswith('ValueError') - excinfo = py.test.raises(ValueError, - "raise ValueError('hello\\nworld')") - msg = excinfo.exconly(tryshort=True) - assert msg.startswith('ValueError') - assert msg.endswith("world") - - -def test_excinfo_repr(): - excinfo = py.test.raises(ValueError, h) - s = repr(excinfo) - assert s == "<ExceptionInfo ValueError tblen=4>" - - -def test_excinfo_str(): - excinfo = py.test.raises(ValueError, h) - s = str(excinfo) - assert s.startswith(__file__[:-9]) # pyc file and $py.class - assert s.endswith("ValueError") - assert len(s.split(":")) >= 3 # on windows it's 4 - - -def test_excinfo_errisinstance(): - excinfo = py.test.raises(ValueError, h) - assert excinfo.errisinstance(ValueError) - - -def test_excinfo_no_sourcecode(): - try: - exec ("raise ValueError()") - except ValueError: - excinfo = py.code.ExceptionInfo() - s = str(excinfo.traceback[-1]) - assert s == " File '<string>':1 in <module>\n ???\n" - - -def test_excinfo_no_python_sourcecode(tmpdir): - #XXX: simplified locally testable version - tmpdir.join('test.txt').write("{{ h()}}:") - - jinja2 = py.test.importorskip('jinja2') - loader = jinja2.FileSystemLoader(str(tmpdir)) - env = jinja2.Environment(loader=loader) - template = env.get_template('test.txt') - excinfo = py.test.raises(ValueError, - template.render, h=h) - for item in excinfo.traceback: - print(item) # XXX: for some reason jinja.Template.render is printed in full - item.source # shouldnt fail - if item.path.basename == 'test.txt': - assert str(item.source) == '{{ h()}}:' - - -def test_entrysource_Queue_example(): - try: - queue.Queue().get(timeout=0.001) - except queue.Empty: - excinfo = py.code.ExceptionInfo() - entry = excinfo.traceback[-1] - source = entry.getsource() - assert source is not None - s = str(source).strip() - assert s.startswith("def get") - - -def test_codepath_Queue_example(): - try: - queue.Queue().get(timeout=0.001) - except queue.Empty: - excinfo = py.code.ExceptionInfo() - entry = excinfo.traceback[-1] - path = entry.path - assert isinstance(path, py.path.local) - assert path.basename.lower() == "queue.py" - assert path.check() - - -class TestFormattedExcinfo: - def pytest_funcarg__importasmod(self, request): - def importasmod(source): - source = py.code.Source(source) - tmpdir = request.getfuncargvalue("tmpdir") - modpath = tmpdir.join("mod.py") - tmpdir.ensure("__init__.py") - modpath.write(source) - if invalidate_import_caches is not None: - invalidate_import_caches() - return modpath.pyimport() - return importasmod - - def excinfo_from_exec(self, source): - source = py.code.Source(source).strip() - try: - exec (source.compile()) - except KeyboardInterrupt: - raise - except: - return py.code.ExceptionInfo() - assert 0, "did not raise" - - def test_repr_source(self): - pr = FormattedExcinfo() - source = py.code.Source(""" - def f(x): - pass - """).strip() - pr.flow_marker = "|" - lines = pr.get_source(source, 0) - assert len(lines) == 2 - assert lines[0] == "| def f(x):" - assert lines[1] == " pass" - - @broken_on_modern_pytest - def test_repr_source_excinfo(self): - """ check if indentation is right """ - pr = FormattedExcinfo() - excinfo = self.excinfo_from_exec(""" - def f(): - assert 0 - f() - """) - pr = FormattedExcinfo() - source = pr._getentrysource(excinfo.traceback[-1]) - lines = pr.get_source(source, 1, excinfo) - assert lines == [ - ' def f():', - '> assert 0', - 'E assert 0' - ] - - def test_repr_source_not_existing(self): - pr = FormattedExcinfo() - co = compile("raise ValueError()", "", "exec") - try: - exec (co) - except ValueError: - excinfo = py.code.ExceptionInfo() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" - - def test_repr_many_line_source_not_existing(self): - pr = FormattedExcinfo() - co = compile(""" -a = 1 -raise ValueError() -""", "", "exec") - try: - exec (co) - except ValueError: - excinfo = py.code.ExceptionInfo() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" - - def test_repr_source_failing_fullsource(self): - pr = FormattedExcinfo() - - class FakeCode(object): - class raw: - co_filename = '?' - path = '?' - firstlineno = 5 - - def fullsource(self): - return None - fullsource = property(fullsource) - - class FakeFrame(object): - code = FakeCode() - f_locals = {} - f_globals = {} - - class FakeTracebackEntry(py.code.Traceback.Entry): - def __init__(self, tb): - self.lineno = 5+3 - - @property - def frame(self): - return FakeFrame() - - class Traceback(py.code.Traceback): - Entry = FakeTracebackEntry - - class FakeExcinfo(py.code.ExceptionInfo): - typename = "Foo" - def __init__(self): - pass - - def exconly(self, tryshort): - return "EXC" - def errisinstance(self, cls): - return False - - excinfo = FakeExcinfo() - class FakeRawTB(object): - tb_next = None - tb = FakeRawTB() - excinfo.traceback = Traceback(tb) - - fail = IOError() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - - fail = py.error.ENOENT - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - - - def test_repr_local(self): - p = FormattedExcinfo(showlocals=True) - loc = {'y': 5, 'z': 7, 'x': 3, '@x': 2, '__builtins__': {}} - reprlocals = p.repr_locals(loc) - assert reprlocals.lines - assert reprlocals.lines[0] == '__builtins__ = <builtins>' - assert reprlocals.lines[1] == 'x = 3' - assert reprlocals.lines[2] == 'y = 5' - assert reprlocals.lines[3] == 'z = 7' - - def test_repr_tracebackentry_lines(self, importasmod): - mod = importasmod(""" - def func1(): - raise ValueError("hello\\nworld") - """) - excinfo = py.test.raises(ValueError, mod.func1) - excinfo.traceback = excinfo.traceback.filter() - p = FormattedExcinfo() - reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) - - # test as intermittent entry - lines = reprtb.lines - assert lines[0] == ' def func1():' - assert lines[1] == '> raise ValueError("hello\\nworld")' - - # test as last entry - p = FormattedExcinfo(showlocals=True) - repr_entry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - lines = repr_entry.lines - assert lines[0] == ' def func1():' - assert lines[1] == '> raise ValueError("hello\\nworld")' - assert lines[2] == 'E ValueError: hello' - assert lines[3] == 'E world' - assert not lines[4:] - - loc = repr_entry.reprlocals is not None - loc = repr_entry.reprfileloc - assert loc.path == mod.__file__ - assert loc.lineno == 3 - #assert loc.message == "ValueError: hello" - - def test_repr_tracebackentry_lines(self, importasmod): - mod = importasmod(""" - def func1(m, x, y, z): - raise ValueError("hello\\nworld") - """) - excinfo = py.test.raises(ValueError, mod.func1, "m"*90, 5, 13, "z"*120) - excinfo.traceback = excinfo.traceback.filter() - entry = excinfo.traceback[-1] - p = FormattedExcinfo(funcargs=True) - reprfuncargs = p.repr_args(entry) - assert reprfuncargs.args[0] == ('m', repr("m"*90)) - assert reprfuncargs.args[1] == ('x', '5') - assert reprfuncargs.args[2] == ('y', '13') - assert reprfuncargs.args[3] == ('z', repr("z" * 120)) - - p = FormattedExcinfo(funcargs=True) - repr_entry = p.repr_traceback_entry(entry) - assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "m = " + repr('m' * 90) - assert tw.lines[1] == "x = 5, y = 13" - assert tw.lines[2] == "z = " + repr('z' * 120) - - def test_repr_tracebackentry_lines_var_kw_args(self, importasmod): - mod = importasmod(""" - def func1(x, *y, **z): - raise ValueError("hello\\nworld") - """) - excinfo = py.test.raises(ValueError, mod.func1, 'a', 'b', c='d') - excinfo.traceback = excinfo.traceback.filter() - entry = excinfo.traceback[-1] - p = FormattedExcinfo(funcargs=True) - reprfuncargs = p.repr_args(entry) - assert reprfuncargs.args[0] == ('x', repr('a')) - assert reprfuncargs.args[1] == ('y', repr(('b',))) - assert reprfuncargs.args[2] == ('z', repr({'c': 'd'})) - - p = FormattedExcinfo(funcargs=True) - repr_entry = p.repr_traceback_entry(entry) - assert repr_entry.reprfuncargs.args == reprfuncargs.args - tw = TWMock() - repr_entry.toterminal(tw) - assert tw.lines[0] == "x = 'a', y = ('b',), z = {'c': 'd'}" - - def test_repr_tracebackentry_short(self, importasmod): - mod = importasmod(""" - def func1(): - raise ValueError("hello") - def entry(): - func1() - """) - excinfo = py.test.raises(ValueError, mod.entry) - p = FormattedExcinfo(style="short") - reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) - lines = reprtb.lines - basename = py.path.local(mod.__file__).basename - assert lines[0] == ' func1()' - assert basename in str(reprtb.reprfileloc.path) - assert reprtb.reprfileloc.lineno == 5 - - # test last entry - p = FormattedExcinfo(style="short") - reprtb = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - lines = reprtb.lines - assert lines[0] == ' raise ValueError("hello")' - assert lines[1] == 'E ValueError: hello' - assert basename in str(reprtb.reprfileloc.path) - assert reprtb.reprfileloc.lineno == 3 - - def test_repr_tracebackentry_no(self, importasmod): - mod = importasmod(""" - def func1(): - raise ValueError("hello") - def entry(): - func1() - """) - excinfo = py.test.raises(ValueError, mod.entry) - p = FormattedExcinfo(style="no") - p.repr_traceback_entry(excinfo.traceback[-2]) - - p = FormattedExcinfo(style="no") - reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - lines = reprentry.lines - assert lines[0] == 'E ValueError: hello' - assert not lines[1:] - - def test_repr_traceback_tbfilter(self, importasmod): - mod = importasmod(""" - def f(x): - raise ValueError(x) - def entry(): - f(0) - """) - excinfo = py.test.raises(ValueError, mod.entry) - p = FormattedExcinfo(tbfilter=True) - reprtb = p.repr_traceback(excinfo) - assert len(reprtb.reprentries) == 2 - p = FormattedExcinfo(tbfilter=False) - reprtb = p.repr_traceback(excinfo) - assert len(reprtb.reprentries) == 3 - - def test_traceback_short_no_source(self, importasmod, monkeypatch): - mod = importasmod(""" - def func1(): - raise ValueError("hello") - def entry(): - func1() - """) - try: - mod.entry() - except ValueError: - excinfo = py.code.ExceptionInfo() - from py._code.code import Code - monkeypatch.setattr(Code, 'path', 'bogus') - excinfo.traceback[0].frame.code.path = "bogus" - p = FormattedExcinfo(style="short") - reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) - lines = reprtb.lines - last_p = FormattedExcinfo(style="short") - last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - last_lines = last_reprtb.lines - monkeypatch.undo() - basename = py.path.local(mod.__file__).basename - assert lines[0] == ' func1()' - - assert last_lines[0] == ' raise ValueError("hello")' - assert last_lines[1] == 'E ValueError: hello' - - def test_repr_traceback_and_excinfo(self, importasmod): - mod = importasmod(""" - def f(x): - raise ValueError(x) - def entry(): - f(0) - """) - excinfo = py.test.raises(ValueError, mod.entry) - - for style in ("long", "short"): - p = FormattedExcinfo(style=style) - reprtb = p.repr_traceback(excinfo) - assert len(reprtb.reprentries) == 2 - assert reprtb.style == style - assert not reprtb.extraline - repr = p.repr_excinfo(excinfo) - assert repr.reprtraceback - assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries) - assert repr.reprcrash.path.endswith("mod.py") - assert repr.reprcrash.message == "ValueError: 0" - - def test_repr_traceback_with_invalid_cwd(self, importasmod, monkeypatch): - mod = importasmod(""" - def f(x): - raise ValueError(x) - def entry(): - f(0) - """) - excinfo = py.test.raises(ValueError, mod.entry) - - p = FormattedExcinfo() - def raiseos(): - raise OSError(2) - monkeypatch.setattr('os.getcwd', raiseos) - assert p._makepath(__file__) == __file__ - reprtb = p.repr_traceback(excinfo) - - @broken_on_modern_pytest - def test_repr_excinfo_addouterr(self, importasmod): - mod = importasmod(""" - def entry(): - raise ValueError() - """) - excinfo = py.test.raises(ValueError, mod.entry) - repr = excinfo.getrepr() - repr.addsection("title", "content") - twmock = TWMock() - repr.toterminal(twmock) - assert twmock.lines[-1] == "content" - assert twmock.lines[-2] == ("-", "title") - - def test_repr_excinfo_reprcrash(self, importasmod): - mod = importasmod(""" - def entry(): - raise ValueError() - """) - excinfo = py.test.raises(ValueError, mod.entry) - repr = excinfo.getrepr() - assert repr.reprcrash.path.endswith("mod.py") - assert repr.reprcrash.lineno == 3 - assert repr.reprcrash.message == "ValueError" - assert str(repr.reprcrash).endswith("mod.py:3: ValueError") - - def test_repr_traceback_recursion(self, importasmod): - mod = importasmod(""" - def rec2(x): - return rec1(x+1) - def rec1(x): - return rec2(x-1) - def entry(): - rec1(42) - """) - excinfo = py.test.raises(RuntimeError, mod.entry) - - for style in ("short", "long", "no"): - p = FormattedExcinfo(style="short") - reprtb = p.repr_traceback(excinfo) - assert reprtb.extraline == "!!! Recursion detected (same locals & position)" - assert str(reprtb) - - @broken_on_modern_pytest - def test_tb_entry_AssertionError(self, importasmod): - # probably this test is a bit redundant - # as py/magic/testing/test_assertion.py - # already tests correctness of - # assertion-reinterpretation logic - mod = importasmod(""" - def somefunc(): - x = 1 - assert x == 2 - """) - excinfo = py.test.raises(AssertionError, mod.somefunc) - - p = FormattedExcinfo() - reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - lines = reprentry.lines - assert lines[-1] == "E assert 1 == 2" - - def test_reprexcinfo_getrepr(self, importasmod): - mod = importasmod(""" - def f(x): - raise ValueError(x) - def entry(): - f(0) - """) - try: - mod.entry() - except ValueError: - excinfo = py.code.ExceptionInfo() - - for style in ("short", "long", "no"): - for showlocals in (True, False): - repr = excinfo.getrepr(style=style, showlocals=showlocals) - assert isinstance(repr, ReprExceptionInfo) - assert repr.reprtraceback.style == style - - def test_reprexcinfo_unicode(self): - from py._code.code import TerminalRepr - class MyRepr(TerminalRepr): - def toterminal(self, tw): - tw.line(py.builtin._totext("я", "utf-8")) - x = py.builtin._totext(MyRepr()) - assert x == py.builtin._totext("я", "utf-8") - - @broken_on_modern_pytest - def test_toterminal_long(self, importasmod): - mod = importasmod(""" - def g(x): - raise ValueError(x) - def f(): - g(3) - """) - excinfo = py.test.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() - repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == " def f():" - assert tw.lines[1] == "> g(3)" - assert tw.lines[2] == "" - assert tw.lines[3].endswith("mod.py:5: ") - assert tw.lines[4] == ("_ ", None) - assert tw.lines[5] == "" - assert tw.lines[6] == " def g(x):" - assert tw.lines[7] == "> raise ValueError(x)" - assert tw.lines[8] == "E ValueError: 3" - assert tw.lines[9] == "" - assert tw.lines[10].endswith("mod.py:3: ValueError") - - @broken_on_modern_pytest - def test_toterminal_long_missing_source(self, importasmod, tmpdir): - mod = importasmod(""" - def g(x): - raise ValueError(x) - def f(): - g(3) - """) - excinfo = py.test.raises(ValueError, mod.f) - tmpdir.join('mod.py').remove() - excinfo.traceback = excinfo.traceback.filter() - repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - assert tw.lines[2].endswith("mod.py:5: ") - assert tw.lines[3] == ("_ ", None) - assert tw.lines[4] == "" - assert tw.lines[5] == "> ???" - assert tw.lines[6] == "E ValueError: 3" - assert tw.lines[7] == "" - assert tw.lines[8].endswith("mod.py:3: ValueError") - - @broken_on_modern_pytest - def test_toterminal_long_incomplete_source(self, importasmod, tmpdir): - mod = importasmod(""" - def g(x): - raise ValueError(x) - def f(): - g(3) - """) - excinfo = py.test.raises(ValueError, mod.f) - tmpdir.join('mod.py').write('asdf') - excinfo.traceback = excinfo.traceback.filter() - repr = excinfo.getrepr() - tw = TWMock() - repr.toterminal(tw) - assert tw.lines[0] == "" - tw.lines.pop(0) - assert tw.lines[0] == "> ???" - assert tw.lines[1] == "" - assert tw.lines[2].endswith("mod.py:5: ") - assert tw.lines[3] == ("_ ", None) - assert tw.lines[4] == "" - assert tw.lines[5] == "> ???" - assert tw.lines[6] == "E ValueError: 3" - assert tw.lines[7] == "" - assert tw.lines[8].endswith("mod.py:3: ValueError") - - @broken_on_modern_pytest - def test_toterminal_long_filenames(self, importasmod): - mod = importasmod(""" - def f(): - raise ValueError() - """) - excinfo = py.test.raises(ValueError, mod.f) - tw = TWMock() - path = py.path.local(mod.__file__) - old = path.dirpath().chdir() - try: - repr = excinfo.getrepr(abspath=False) - repr.toterminal(tw) - line = tw.lines[-1] - x = py.path.local().bestrelpath(path) - if len(x) < len(str(path)): - assert line == "mod.py:3: ValueError" - - repr = excinfo.getrepr(abspath=True) - repr.toterminal(tw) - line = tw.lines[-1] - assert line == "%s:3: ValueError" %(path,) - finally: - old.chdir() - - @pytest.mark.parametrize('style', ("long", "short", "no")) - @pytest.mark.parametrize('showlocals', (True, False), - ids=['locals', 'nolocals']) - @pytest.mark.parametrize('tbfilter', (True, False), - ids=['tbfilter', 'nofilter']) - @pytest.mark.parametrize('funcargs', (True, False), - ids=['funcargs', 'nofuncargs']) - def test_format_excinfo(self, importasmod, - style, showlocals, tbfilter, funcargs): - - mod = importasmod(""" - def g(x): - raise ValueError(x) - def f(): - g(3) - """) - excinfo = py.test.raises(ValueError, mod.f) - tw = py.io.TerminalWriter(stringio=True) - repr = excinfo.getrepr( - style=style, - showlocals=showlocals, - funcargs=funcargs, - tbfilter=tbfilter - ) - repr.toterminal(tw) - assert tw.stringio.getvalue() - - @broken_on_modern_pytest - def test_native_style(self): - excinfo = self.excinfo_from_exec(""" - assert 0 - """) - repr = excinfo.getrepr(style='native') - assert "assert 0" in str(repr.reprcrash) - s = str(repr) - assert s.startswith('Traceback (most recent call last):\n File') - assert s.endswith('\nAssertionError: assert 0') - assert 'exec (source.compile())' in s - assert s.count('assert 0') == 2 - - @broken_on_modern_pytest - def test_traceback_repr_style(self, importasmod): - mod = importasmod(""" - def f(): - g() - def g(): - h() - def h(): - i() - def i(): - raise ValueError() - """) - excinfo = py.test.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() - excinfo.traceback[1].set_repr_style("short") - excinfo.traceback[2].set_repr_style("short") - r = excinfo.getrepr(style="long") - tw = TWMock() - r.toterminal(tw) - for line in tw.lines: print (line) - assert tw.lines[0] == "" - assert tw.lines[1] == " def f():" - assert tw.lines[2] == "> g()" - assert tw.lines[3] == "" - assert tw.lines[4].endswith("mod.py:3: ") - assert tw.lines[5] == ("_ ", None) - assert tw.lines[6].endswith("in g") - assert tw.lines[7] == " h()" - assert tw.lines[8].endswith("in h") - assert tw.lines[9] == " i()" - assert tw.lines[10] == ("_ ", None) - assert tw.lines[11] == "" - assert tw.lines[12] == " def i():" - assert tw.lines[13] == "> raise ValueError()" - assert tw.lines[14] == "E ValueError" - assert tw.lines[15] == "" - assert tw.lines[16].endswith("mod.py:9: ValueError") diff --git a/tests/wpt/tests/tools/third_party/py/testing/code/test_source.py b/tests/wpt/tests/tools/third_party/py/testing/code/test_source.py deleted file mode 100644 index ca9a42275cf..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/code/test_source.py +++ /dev/null @@ -1,656 +0,0 @@ -from py.code import Source -import py -import sys -import inspect - -from py._code.source import _ast -if _ast is not None: - astonly = py.test.mark.nothing -else: - astonly = py.test.mark.xfail("True", reason="only works with AST-compile") - -failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") - -def test_source_str_function(): - x = Source("3") - assert str(x) == "3" - - x = Source(" 3") - assert str(x) == "3" - - x = Source(""" - 3 - """, rstrip=False) - assert str(x) == "\n3\n " - - x = Source(""" - 3 - """, rstrip=True) - assert str(x) == "\n3" - -def test_unicode(): - try: - unicode - except NameError: - return - x = Source(unicode("4")) - assert str(x) == "4" - co = py.code.compile(unicode('u"\xc3\xa5"', 'utf8'), mode='eval') - val = eval(co) - assert isinstance(val, unicode) - -def test_source_from_function(): - source = py.code.Source(test_source_str_function) - assert str(source).startswith('def test_source_str_function():') - -def test_source_from_method(): - class TestClass: - def test_method(self): - pass - source = py.code.Source(TestClass().test_method) - assert source.lines == ["def test_method(self):", - " pass"] - -def test_source_from_lines(): - lines = ["a \n", "b\n", "c"] - source = py.code.Source(lines) - assert source.lines == ['a ', 'b', 'c'] - -def test_source_from_inner_function(): - def f(): - pass - source = py.code.Source(f, deindent=False) - assert str(source).startswith(' def f():') - source = py.code.Source(f) - assert str(source).startswith('def f():') - -def test_source_putaround_simple(): - source = Source("raise ValueError") - source = source.putaround( - "try:", """\ - except ValueError: - x = 42 - else: - x = 23""") - assert str(source)=="""\ -try: - raise ValueError -except ValueError: - x = 42 -else: - x = 23""" - -def test_source_putaround(): - source = Source() - source = source.putaround(""" - if 1: - x=1 - """) - assert str(source).strip() == "if 1:\n x=1" - -def test_source_strips(): - source = Source("") - assert source == Source() - assert str(source) == '' - assert source.strip() == source - -def test_source_strip_multiline(): - source = Source() - source.lines = ["", " hello", " "] - source2 = source.strip() - assert source2.lines == [" hello"] - -def test_syntaxerror_rerepresentation(): - ex = py.test.raises(SyntaxError, py.code.compile, 'xyz xyz') - assert ex.value.lineno == 1 - assert ex.value.offset in (5, 7) # pypy/cpython difference - assert ex.value.text.strip(), 'x x' - -def test_isparseable(): - assert Source("hello").isparseable() - assert Source("if 1:\n pass").isparseable() - assert Source(" \nif 1:\n pass").isparseable() - assert not Source("if 1:\n").isparseable() - assert not Source(" \nif 1:\npass").isparseable() - assert not Source(chr(0)).isparseable() - -class TestAccesses: - source = Source("""\ - def f(x): - pass - def g(x): - pass - """) - def test_getrange(self): - x = self.source[0:2] - assert x.isparseable() - assert len(x.lines) == 2 - assert str(x) == "def f(x):\n pass" - - def test_getline(self): - x = self.source[0] - assert x == "def f(x):" - - def test_len(self): - assert len(self.source) == 4 - - def test_iter(self): - l = [x for x in self.source] - assert len(l) == 4 - -class TestSourceParsingAndCompiling: - source = Source("""\ - def f(x): - assert (x == - 3 + - 4) - """).strip() - - def test_compile(self): - co = py.code.compile("x=3") - d = {} - exec (co, d) - assert d['x'] == 3 - - def test_compile_and_getsource_simple(self): - co = py.code.compile("x=3") - exec (co) - source = py.code.Source(co) - assert str(source) == "x=3" - - def test_compile_and_getsource_through_same_function(self): - def gensource(source): - return py.code.compile(source) - co1 = gensource(""" - def f(): - raise KeyError() - """) - co2 = gensource(""" - def f(): - raise ValueError() - """) - source1 = inspect.getsource(co1) - assert 'KeyError' in source1 - source2 = inspect.getsource(co2) - assert 'ValueError' in source2 - - def test_getstatement(self): - #print str(self.source) - ass = str(self.source[1:]) - for i in range(1, 4): - #print "trying start in line %r" % self.source[i] - s = self.source.getstatement(i) - #x = s.deindent() - assert str(s) == ass - - def test_getstatementrange_triple_quoted(self): - #print str(self.source) - source = Source("""hello(''' - ''')""") - s = source.getstatement(0) - assert s == str(source) - s = source.getstatement(1) - assert s == str(source) - - @astonly - def test_getstatementrange_within_constructs(self): - source = Source("""\ - try: - try: - raise ValueError - except SomeThing: - pass - finally: - 42 - """) - assert len(source) == 7 - # check all lineno's that could occur in a traceback - #assert source.getstatementrange(0) == (0, 7) - #assert source.getstatementrange(1) == (1, 5) - assert source.getstatementrange(2) == (2, 3) - assert source.getstatementrange(3) == (3, 4) - assert source.getstatementrange(4) == (4, 5) - #assert source.getstatementrange(5) == (0, 7) - assert source.getstatementrange(6) == (6, 7) - - def test_getstatementrange_bug(self): - source = Source("""\ - try: - x = ( - y + - z) - except: - pass - """) - assert len(source) == 6 - assert source.getstatementrange(2) == (1, 4) - - def test_getstatementrange_bug2(self): - source = Source("""\ - assert ( - 33 - == - [ - X(3, - b=1, c=2 - ), - ] - ) - """) - assert len(source) == 9 - assert source.getstatementrange(5) == (0, 9) - - def test_getstatementrange_ast_issue58(self): - source = Source("""\ - - def test_some(): - for a in [a for a in - CAUSE_ERROR]: pass - - x = 3 - """) - assert getstatement(2, source).lines == source.lines[2:3] - assert getstatement(3, source).lines == source.lines[3:4] - - def test_getstatementrange_out_of_bounds_py3(self): - source = Source("if xxx:\n from .collections import something") - r = source.getstatementrange(1) - assert r == (1,2) - - def test_getstatementrange_with_syntaxerror_issue7(self): - source = Source(":") - py.test.raises(SyntaxError, lambda: source.getstatementrange(0)) - - def test_compile_to_ast(self): - import ast - source = Source("x = 4") - mod = source.compile(flag=ast.PyCF_ONLY_AST) - assert isinstance(mod, ast.Module) - compile(mod, "<filename>", "exec") - - def test_compile_and_getsource(self): - co = self.source.compile() - py.builtin.exec_(co, globals()) - f(7) - excinfo = py.test.raises(AssertionError, "f(6)") - frame = excinfo.traceback[-1].frame - stmt = frame.code.fullsource.getstatement(frame.lineno) - #print "block", str(block) - assert str(stmt).strip().startswith('assert') - - def test_compilefuncs_and_path_sanity(self): - def check(comp, name): - co = comp(self.source, name) - if not name: - expected = "codegen %s:%d>" %(mypath, mylineno+2+1) - else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno+2+1) - fn = co.co_filename - assert fn.endswith(expected) - - mycode = py.code.Code(self.test_compilefuncs_and_path_sanity) - mylineno = mycode.firstlineno - mypath = mycode.path - - for comp in py.code.compile, py.code.Source.compile: - for name in '', None, 'my': - yield check, comp, name - - def test_offsetless_synerr(self): - py.test.raises(SyntaxError, py.code.compile, "lambda a,a: 0", mode='eval') - -def test_getstartingblock_singleline(): - class A: - def __init__(self, *args): - frame = sys._getframe(1) - self.source = py.code.Frame(frame).statement - - x = A('x', 'y') - - l = [i for i in x.source.lines if i.strip()] - assert len(l) == 1 - -def test_getstartingblock_multiline(): - class A: - def __init__(self, *args): - frame = sys._getframe(1) - self.source = py.code.Frame(frame).statement - - x = A('x', - 'y' \ - , - 'z') - - l = [i for i in x.source.lines if i.strip()] - assert len(l) == 4 - -def test_getline_finally(): - def c(): pass - excinfo = py.test.raises(TypeError, """ - teardown = None - try: - c(1) - finally: - if teardown: - teardown() - """) - source = excinfo.traceback[-1].statement - assert str(source).strip() == 'c(1)' - -def test_getfuncsource_dynamic(): - source = """ - def f(): - raise ValueError - - def g(): pass - """ - co = py.code.compile(source) - py.builtin.exec_(co, globals()) - assert str(py.code.Source(f)).strip() == 'def f():\n raise ValueError' - assert str(py.code.Source(g)).strip() == 'def g(): pass' - - -def test_getfuncsource_with_multine_string(): - def f(): - c = '''while True: - pass -''' - assert str(py.code.Source(f)).strip() == "def f():\n c = '''while True:\n pass\n'''" - - -def test_deindent(): - from py._code.source import deindent as deindent - assert deindent(['\tfoo', '\tbar', ]) == ['foo', 'bar'] - - def f(): - c = '''while True: - pass -''' - import inspect - lines = deindent(inspect.getsource(f).splitlines()) - assert lines == ["def f():", " c = '''while True:", " pass", "'''"] - - source = """ - def f(): - def g(): - pass - """ - lines = deindent(source.splitlines()) - assert lines == ['', 'def f():', ' def g():', ' pass', ' '] - -def test_source_of_class_at_eof_without_newline(tmpdir): - # this test fails because the implicit inspect.getsource(A) below - # does not return the "x = 1" last line. - source = py.code.Source(''' - class A(object): - def method(self): - x = 1 - ''') - path = tmpdir.join("a.py") - path.write(source) - s2 = py.code.Source(tmpdir.join("a.py").pyimport().A) - assert str(source).strip() == str(s2).strip() - -if True: - def x(): - pass - -def test_getsource_fallback(): - from py._code.source import getsource - expected = """def x(): - pass""" - src = getsource(x) - assert src == expected - -def test_idem_compile_and_getsource(): - from py._code.source import getsource - expected = "def x(): pass" - co = py.code.compile(expected) - src = getsource(co) - assert src == expected - -def test_findsource_fallback(): - from py._code.source import findsource - src, lineno = findsource(x) - assert 'test_findsource_simple' in str(src) - assert src[lineno] == ' def x():' - -def test_findsource(): - from py._code.source import findsource - co = py.code.compile("""if 1: - def x(): - pass -""") - - src, lineno = findsource(co) - assert 'if 1:' in str(src) - - d = {} - eval(co, d) - src, lineno = findsource(d['x']) - assert 'if 1:' in str(src) - assert src[lineno] == " def x():" - - -def test_getfslineno(): - from py.code import getfslineno - - def f(x): - pass - - fspath, lineno = getfslineno(f) - - assert fspath.basename == "test_source.py" - assert lineno == py.code.getrawcode(f).co_firstlineno-1 # see findsource - - class A(object): - pass - - fspath, lineno = getfslineno(A) - - _, A_lineno = inspect.findsource(A) - assert fspath.basename == "test_source.py" - assert lineno == A_lineno - - assert getfslineno(3) == ("", -1) - class B: - pass - B.__name__ = "B2" - # TODO: On CPython 3.9 this actually returns the line, - # should it? - # assert getfslineno(B)[1] == -1 - -def test_code_of_object_instance_with_call(): - class A: - pass - py.test.raises(TypeError, lambda: py.code.Source(A())) - class WithCall: - def __call__(self): - pass - - code = py.code.Code(WithCall()) - assert 'pass' in str(code.source()) - - class Hello(object): - def __call__(self): - pass - py.test.raises(TypeError, lambda: py.code.Code(Hello)) - - -def getstatement(lineno, source): - from py._code.source import getstatementrange_ast - source = py.code.Source(source, deindent=False) - ast, start, end = getstatementrange_ast(lineno, source) - return source[start:end] - -def test_oneline(): - source = getstatement(0, "raise ValueError") - assert str(source) == "raise ValueError" - -def test_comment_and_no_newline_at_end(): - from py._code.source import getstatementrange_ast - source = Source(['def test_basic_complex():', - ' assert 1 == 2', - '# vim: filetype=pyopencl:fdm=marker']) - ast, start, end = getstatementrange_ast(1, source) - assert end == 2 - -def test_oneline_and_comment(): - source = getstatement(0, "raise ValueError\n#hello") - assert str(source) == "raise ValueError" - -def test_comments(): - source = '''def test(): - "comment 1" - x = 1 - # comment 2 - # comment 3 - - assert False - -""" -comment 4 -""" -''' - for line in range(2, 6): - assert str(getstatement(line, source)) == " x = 1" - if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - tqs_start = 8 - else: - tqs_start = 10 - assert str(getstatement(10, source)) == '"""' - for line in range(6, tqs_start): - assert str(getstatement(line, source)) == " assert False" - for line in range(tqs_start, 10): - assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' - -def test_comment_in_statement(): - source = '''test(foo=1, - # comment 1 - bar=2) -''' - for line in range(1,3): - assert str(getstatement(line, source)) == \ - 'test(foo=1,\n # comment 1\n bar=2)' - -def test_single_line_else(): - source = getstatement(1, "if False: 2\nelse: 3") - assert str(source) == "else: 3" - -def test_single_line_finally(): - source = getstatement(1, "try: 1\nfinally: 3") - assert str(source) == "finally: 3" - -def test_issue55(): - source = ('def round_trip(dinp):\n assert 1 == dinp\n' - 'def test_rt():\n round_trip("""\n""")\n') - s = getstatement(3, source) - assert str(s) == ' round_trip("""\n""")' - - -def XXXtest_multiline(): - source = getstatement(0, """\ -raise ValueError( - 23 -) -x = 3 -""") - assert str(source) == "raise ValueError(\n 23\n)" - -class TestTry: - pytestmark = astonly - source = """\ -try: - raise ValueError -except Something: - raise IndexError(1) -else: - raise KeyError() -""" - - def test_body(self): - source = getstatement(1, self.source) - assert str(source) == " raise ValueError" - - def test_except_line(self): - source = getstatement(2, self.source) - assert str(source) == "except Something:" - - def test_except_body(self): - source = getstatement(3, self.source) - assert str(source) == " raise IndexError(1)" - - def test_else(self): - source = getstatement(5, self.source) - assert str(source) == " raise KeyError()" - -class TestTryFinally: - source = """\ -try: - raise ValueError -finally: - raise IndexError(1) -""" - - def test_body(self): - source = getstatement(1, self.source) - assert str(source) == " raise ValueError" - - def test_finally(self): - source = getstatement(3, self.source) - assert str(source) == " raise IndexError(1)" - - - -class TestIf: - pytestmark = astonly - source = """\ -if 1: - y = 3 -elif False: - y = 5 -else: - y = 7 -""" - - def test_body(self): - source = getstatement(1, self.source) - assert str(source) == " y = 3" - - def test_elif_clause(self): - source = getstatement(2, self.source) - assert str(source) == "elif False:" - - def test_elif(self): - source = getstatement(3, self.source) - assert str(source) == " y = 5" - - def test_else(self): - source = getstatement(5, self.source) - assert str(source) == " y = 7" - -def test_semicolon(): - s = """\ -hello ; pytest.skip() -""" - source = getstatement(0, s) - assert str(source) == s.strip() - -def test_def_online(): - s = """\ -def func(): raise ValueError(42) - -def something(): - pass -""" - source = getstatement(0, s) - assert str(source) == "def func(): raise ValueError(42)" - -def XXX_test_expression_multiline(): - source = """\ -something -''' -'''""" - result = getstatement(1, source) - assert str(result) == "'''\n'''" - diff --git a/tests/wpt/tests/tools/third_party/py/testing/conftest.py b/tests/wpt/tests/tools/third_party/py/testing/conftest.py deleted file mode 100644 index 0f956b3dd25..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ - -pytest_plugins = "pytester", - diff --git a/tests/wpt/tests/tools/third_party/py/testing/io_/__init__.py b/tests/wpt/tests/tools/third_party/py/testing/io_/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/io_/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/wpt/tests/tools/third_party/py/testing/io_/test_capture.py b/tests/wpt/tests/tools/third_party/py/testing/io_/test_capture.py deleted file mode 100644 index b5fedd0abc6..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/io_/test_capture.py +++ /dev/null @@ -1,501 +0,0 @@ -from __future__ import with_statement - -import os, sys -import py - -needsdup = py.test.mark.skipif("not hasattr(os, 'dup')") - -from py.builtin import print_ - -if sys.version_info >= (3,0): - def tobytes(obj): - if isinstance(obj, str): - obj = obj.encode('UTF-8') - assert isinstance(obj, bytes) - return obj - def totext(obj): - if isinstance(obj, bytes): - obj = str(obj, 'UTF-8') - assert isinstance(obj, str) - return obj -else: - def tobytes(obj): - if isinstance(obj, unicode): - obj = obj.encode('UTF-8') - assert isinstance(obj, str) - return obj - def totext(obj): - if isinstance(obj, str): - obj = unicode(obj, 'UTF-8') - assert isinstance(obj, unicode) - return obj - -def oswritebytes(fd, obj): - os.write(fd, tobytes(obj)) - -class TestTextIO: - def test_text(self): - f = py.io.TextIO() - f.write("hello") - s = f.getvalue() - assert s == "hello" - f.close() - - def test_unicode_and_str_mixture(self): - f = py.io.TextIO() - if sys.version_info >= (3,0): - f.write("\u00f6") - py.test.raises(TypeError, "f.write(bytes('hello', 'UTF-8'))") - else: - f.write(unicode("\u00f6", 'UTF-8')) - f.write("hello") # bytes - s = f.getvalue() - f.close() - assert isinstance(s, unicode) - -def test_bytes_io(): - f = py.io.BytesIO() - f.write(tobytes("hello")) - py.test.raises(TypeError, "f.write(totext('hello'))") - s = f.getvalue() - assert s == tobytes("hello") - -def test_dontreadfrominput(): - from py._io.capture import DontReadFromInput - f = DontReadFromInput() - assert not f.isatty() - py.test.raises(IOError, f.read) - py.test.raises(IOError, f.readlines) - py.test.raises(IOError, iter, f) - py.test.raises(ValueError, f.fileno) - f.close() # just for completeness - -def pytest_funcarg__tmpfile(request): - testdir = request.getfuncargvalue("testdir") - f = testdir.makepyfile("").open('wb+') - request.addfinalizer(f.close) - return f - -@needsdup -def test_dupfile(tmpfile): - flist = [] - for i in range(5): - nf = py.io.dupfile(tmpfile, encoding="utf-8") - assert nf != tmpfile - assert nf.fileno() != tmpfile.fileno() - assert nf not in flist - print_(i, end="", file=nf) - flist.append(nf) - for i in range(5): - f = flist[i] - f.close() - tmpfile.seek(0) - s = tmpfile.read() - assert "01234" in repr(s) - tmpfile.close() - -def test_dupfile_no_mode(): - """ - dupfile should trap an AttributeError and return f if no mode is supplied. - """ - class SomeFileWrapper(object): - "An object with a fileno method but no mode attribute" - def fileno(self): - return 1 - tmpfile = SomeFileWrapper() - assert py.io.dupfile(tmpfile) is tmpfile - with py.test.raises(AttributeError): - py.io.dupfile(tmpfile, raising=True) - -def lsof_check(func): - pid = os.getpid() - try: - out = py.process.cmdexec("lsof -p %d" % pid) - except py.process.cmdexec.Error: - py.test.skip("could not run 'lsof'") - func() - out2 = py.process.cmdexec("lsof -p %d" % pid) - len1 = len([x for x in out.split("\n") if "REG" in x]) - len2 = len([x for x in out2.split("\n") if "REG" in x]) - assert len2 < len1 + 3, out2 - -class TestFDCapture: - pytestmark = needsdup - - def test_not_now(self, tmpfile): - fd = tmpfile.fileno() - cap = py.io.FDCapture(fd, now=False) - data = tobytes("hello") - os.write(fd, data) - f = cap.done() - s = f.read() - assert not s - cap = py.io.FDCapture(fd, now=False) - cap.start() - os.write(fd, data) - f = cap.done() - s = f.read() - assert s == "hello" - - def test_simple(self, tmpfile): - fd = tmpfile.fileno() - cap = py.io.FDCapture(fd) - data = tobytes("hello") - os.write(fd, data) - f = cap.done() - s = f.read() - assert s == "hello" - f.close() - - def test_simple_many(self, tmpfile): - for i in range(10): - self.test_simple(tmpfile) - - def test_simple_many_check_open_files(self, tmpfile): - lsof_check(lambda: self.test_simple_many(tmpfile)) - - def test_simple_fail_second_start(self, tmpfile): - fd = tmpfile.fileno() - cap = py.io.FDCapture(fd) - f = cap.done() - py.test.raises(ValueError, cap.start) - f.close() - - def test_stderr(self): - cap = py.io.FDCapture(2, patchsys=True) - print_("hello", file=sys.stderr) - f = cap.done() - s = f.read() - assert s == "hello\n" - - def test_stdin(self, tmpfile): - tmpfile.write(tobytes("3")) - tmpfile.seek(0) - cap = py.io.FDCapture(0, tmpfile=tmpfile) - # check with os.read() directly instead of raw_input(), because - # sys.stdin itself may be redirected (as py.test now does by default) - x = os.read(0, 100).strip() - f = cap.done() - assert x == tobytes("3") - - def test_writeorg(self, tmpfile): - data1, data2 = tobytes("foo"), tobytes("bar") - try: - cap = py.io.FDCapture(tmpfile.fileno()) - tmpfile.write(data1) - cap.writeorg(data2) - finally: - tmpfile.close() - f = cap.done() - scap = f.read() - assert scap == totext(data1) - stmp = open(tmpfile.name, 'rb').read() - assert stmp == data2 - - -class TestStdCapture: - def getcapture(self, **kw): - return py.io.StdCapture(**kw) - - def test_capturing_done_simple(self): - cap = self.getcapture() - sys.stdout.write("hello") - sys.stderr.write("world") - outfile, errfile = cap.done() - s = outfile.read() - assert s == "hello" - s = errfile.read() - assert s == "world" - - def test_capturing_reset_simple(self): - cap = self.getcapture() - print("hello world") - sys.stderr.write("hello error\n") - out, err = cap.reset() - assert out == "hello world\n" - assert err == "hello error\n" - - def test_capturing_readouterr(self): - cap = self.getcapture() - try: - print ("hello world") - sys.stderr.write("hello error\n") - out, err = cap.readouterr() - assert out == "hello world\n" - assert err == "hello error\n" - sys.stderr.write("error2") - finally: - out, err = cap.reset() - assert err == "error2" - - def test_capturing_readouterr_unicode(self): - cap = self.getcapture() - print ("hx\xc4\x85\xc4\x87") - out, err = cap.readouterr() - assert out == py.builtin._totext("hx\xc4\x85\xc4\x87\n", "utf8") - - @py.test.mark.skipif('sys.version_info >= (3,)', - reason='text output different for bytes on python3') - def test_capturing_readouterr_decode_error_handling(self): - cap = self.getcapture() - # triggered a internal error in pytest - print('\xa6') - out, err = cap.readouterr() - assert out == py.builtin._totext('\ufffd\n', 'unicode-escape') - - def test_capturing_mixed(self): - cap = self.getcapture(mixed=True) - sys.stdout.write("hello ") - sys.stderr.write("world") - sys.stdout.write(".") - out, err = cap.reset() - assert out.strip() == "hello world." - assert not err - - def test_reset_twice_error(self): - cap = self.getcapture() - print ("hello") - out, err = cap.reset() - py.test.raises(ValueError, cap.reset) - assert out == "hello\n" - assert not err - - def test_capturing_modify_sysouterr_in_between(self): - oldout = sys.stdout - olderr = sys.stderr - cap = self.getcapture() - sys.stdout.write("hello") - sys.stderr.write("world") - sys.stdout = py.io.TextIO() - sys.stderr = py.io.TextIO() - print ("not seen") - sys.stderr.write("not seen\n") - out, err = cap.reset() - assert out == "hello" - assert err == "world" - assert sys.stdout == oldout - assert sys.stderr == olderr - - def test_capturing_error_recursive(self): - cap1 = self.getcapture() - print ("cap1") - cap2 = self.getcapture() - print ("cap2") - out2, err2 = cap2.reset() - out1, err1 = cap1.reset() - assert out1 == "cap1\n" - assert out2 == "cap2\n" - - def test_just_out_capture(self): - cap = self.getcapture(out=True, err=False) - sys.stdout.write("hello") - sys.stderr.write("world") - out, err = cap.reset() - assert out == "hello" - assert not err - - def test_just_err_capture(self): - cap = self.getcapture(out=False, err=True) - sys.stdout.write("hello") - sys.stderr.write("world") - out, err = cap.reset() - assert err == "world" - assert not out - - def test_stdin_restored(self): - old = sys.stdin - cap = self.getcapture(in_=True) - newstdin = sys.stdin - out, err = cap.reset() - assert newstdin != sys.stdin - assert sys.stdin is old - - def test_stdin_nulled_by_default(self): - print ("XXX this test may well hang instead of crashing") - print ("XXX which indicates an error in the underlying capturing") - print ("XXX mechanisms") - cap = self.getcapture() - py.test.raises(IOError, "sys.stdin.read()") - out, err = cap.reset() - - def test_suspend_resume(self): - cap = self.getcapture(out=True, err=False, in_=False) - try: - print ("hello") - sys.stderr.write("error\n") - out, err = cap.suspend() - assert out == "hello\n" - assert not err - print ("in between") - sys.stderr.write("in between\n") - cap.resume() - print ("after") - sys.stderr.write("error_after\n") - finally: - out, err = cap.reset() - assert out == "after\n" - assert not err - -class TestStdCaptureNotNow(TestStdCapture): - def getcapture(self, **kw): - kw['now'] = False - cap = py.io.StdCapture(**kw) - cap.startall() - return cap - -class TestStdCaptureFD(TestStdCapture): - pytestmark = needsdup - - def getcapture(self, **kw): - return py.io.StdCaptureFD(**kw) - - def test_intermingling(self): - cap = self.getcapture() - oswritebytes(1, "1") - sys.stdout.write(str(2)) - sys.stdout.flush() - oswritebytes(1, "3") - oswritebytes(2, "a") - sys.stderr.write("b") - sys.stderr.flush() - oswritebytes(2, "c") - out, err = cap.reset() - assert out == "123" - assert err == "abc" - - def test_callcapture(self): - def func(x, y): - print (x) - sys.stderr.write(str(y)) - return 42 - - res, out, err = py.io.StdCaptureFD.call(func, 3, y=4) - assert res == 42 - assert out.startswith("3") - assert err.startswith("4") - - def test_many(self, capfd): - def f(): - for i in range(10): - cap = py.io.StdCaptureFD() - cap.reset() - lsof_check(f) - -class TestStdCaptureFDNotNow(TestStdCaptureFD): - pytestmark = needsdup - - def getcapture(self, **kw): - kw['now'] = False - cap = py.io.StdCaptureFD(**kw) - cap.startall() - return cap - -@needsdup -def test_stdcapture_fd_tmpfile(tmpfile): - capfd = py.io.StdCaptureFD(out=tmpfile) - os.write(1, "hello".encode("ascii")) - os.write(2, "world".encode("ascii")) - outf, errf = capfd.done() - assert outf == tmpfile - -class TestStdCaptureFDinvalidFD: - pytestmark = needsdup - def test_stdcapture_fd_invalid_fd(self, testdir): - testdir.makepyfile(""" - import py, os - def test_stdout(): - os.close(1) - cap = py.io.StdCaptureFD(out=True, err=False, in_=False) - cap.done() - def test_stderr(): - os.close(2) - cap = py.io.StdCaptureFD(out=False, err=True, in_=False) - cap.done() - def test_stdin(): - os.close(0) - cap = py.io.StdCaptureFD(out=False, err=False, in_=True) - cap.done() - """) - result = testdir.runpytest("--capture=fd") - assert result.ret == 0 - assert result.parseoutcomes()['passed'] == 3 - -def test_capture_not_started_but_reset(): - capsys = py.io.StdCapture(now=False) - capsys.done() - capsys.done() - capsys.reset() - -@needsdup -def test_capture_no_sys(): - capsys = py.io.StdCapture() - try: - cap = py.io.StdCaptureFD(patchsys=False) - sys.stdout.write("hello") - sys.stderr.write("world") - oswritebytes(1, "1") - oswritebytes(2, "2") - out, err = cap.reset() - assert out == "1" - assert err == "2" - finally: - capsys.reset() - -@needsdup -def test_callcapture_nofd(): - def func(x, y): - oswritebytes(1, "hello") - oswritebytes(2, "hello") - print (x) - sys.stderr.write(str(y)) - return 42 - - capfd = py.io.StdCaptureFD(patchsys=False) - try: - res, out, err = py.io.StdCapture.call(func, 3, y=4) - finally: - capfd.reset() - assert res == 42 - assert out.startswith("3") - assert err.startswith("4") - -@needsdup -@py.test.mark.parametrize('use', [True, False]) -def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): - if not use: - tmpfile = True - cap = py.io.StdCaptureFD(out=False, err=tmpfile, now=False) - cap.startall() - capfile = cap.err.tmpfile - cap.suspend() - cap.resume() - capfile2 = cap.err.tmpfile - assert capfile2 == capfile - -@py.test.mark.parametrize('method', ['StdCapture', 'StdCaptureFD']) -def test_capturing_and_logging_fundamentals(testdir, method): - if method == "StdCaptureFD" and not hasattr(os, 'dup'): - py.test.skip("need os.dup") - # here we check a fundamental feature - p = testdir.makepyfile(""" - import sys, os - import py, logging - cap = py.io.%s(out=False, in_=False) - - logging.warn("hello1") - outerr = cap.suspend() - print ("suspend, captured %%s" %%(outerr,)) - logging.warn("hello2") - - cap.resume() - logging.warn("hello3") - - outerr = cap.suspend() - print ("suspend2, captured %%s" %% (outerr,)) - """ % (method,)) - result = testdir.runpython(p) - result.stdout.fnmatch_lines([ - "suspend, captured*hello1*", - "suspend2, captured*hello2*WARNING:root:hello3*", - ]) - assert "atexit" not in result.stderr.str() diff --git a/tests/wpt/tests/tools/third_party/py/testing/io_/test_saferepr.py b/tests/wpt/tests/tools/third_party/py/testing/io_/test_saferepr.py deleted file mode 100644 index 97be1416fec..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/io_/test_saferepr.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import generators -import py -import sys - -saferepr = py.io.saferepr - -class TestSafeRepr: - def test_simple_repr(self): - assert saferepr(1) == '1' - assert saferepr(None) == 'None' - - def test_maxsize(self): - s = saferepr('x'*50, maxsize=25) - assert len(s) == 25 - expected = repr('x'*10 + '...' + 'x'*10) - assert s == expected - - def test_maxsize_error_on_instance(self): - class A: - def __repr__(self): - raise ValueError('...') - - s = saferepr(('*'*50, A()), maxsize=25) - assert len(s) == 25 - assert s[0] == '(' and s[-1] == ')' - - def test_exceptions(self): - class BrokenRepr: - def __init__(self, ex): - self.ex = ex - foo = 0 - def __repr__(self): - raise self.ex - class BrokenReprException(Exception): - __str__ = None - __repr__ = None - assert 'Exception' in saferepr(BrokenRepr(Exception("broken"))) - s = saferepr(BrokenReprException("really broken")) - assert 'TypeError' in s - assert 'TypeError' in saferepr(BrokenRepr("string")) - - s2 = saferepr(BrokenRepr(BrokenReprException('omg even worse'))) - assert 'NameError' not in s2 - assert 'unknown' in s2 - - def test_big_repr(self): - from py._io.saferepr import SafeRepr - assert len(saferepr(range(1000))) <= \ - len('[' + SafeRepr().maxlist * "1000" + ']') - - def test_repr_on_newstyle(self): - class Function(object): - def __repr__(self): - return "<%s>" %(self.name) - try: - s = saferepr(Function()) - except Exception: - py.test.fail("saferepr failed for newstyle class") - - def test_unicode(self): - val = py.builtin._totext('£€', 'utf-8') - reprval = py.builtin._totext("'£€'", 'utf-8') - assert saferepr(val) == reprval - -def test_unicode_handling(): - value = py.builtin._totext('\xc4\x85\xc4\x87\n', 'utf-8').encode('utf8') - def f(): - raise Exception(value) - excinfo = py.test.raises(Exception, f) - s = str(excinfo) - if sys.version_info[0] < 3: - u = unicode(excinfo) - diff --git a/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter.py b/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter.py deleted file mode 100644 index 44b4f1ddeec..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter.py +++ /dev/null @@ -1,341 +0,0 @@ -from collections import namedtuple - -import py -import os, sys -from py._io import terminalwriter -import codecs -import pytest - -def test_get_terminal_width(): - x = py.io.get_terminal_width - assert x == terminalwriter.get_terminal_width - -def test_getdimensions(monkeypatch): - if sys.version_info >= (3, 3): - import shutil - Size = namedtuple('Size', 'lines columns') - monkeypatch.setattr(shutil, 'get_terminal_size', lambda: Size(60, 100)) - assert terminalwriter._getdimensions() == (60, 100) - else: - fcntl = py.test.importorskip("fcntl") - import struct - l = [] - monkeypatch.setattr(fcntl, 'ioctl', lambda *args: l.append(args)) - try: - terminalwriter._getdimensions() - except (TypeError, struct.error): - pass - assert len(l) == 1 - assert l[0][0] == 1 - -def test_terminal_width_COLUMNS(monkeypatch): - """ Dummy test for get_terminal_width - """ - fcntl = py.test.importorskip("fcntl") - monkeypatch.setattr(fcntl, 'ioctl', lambda *args: int('x')) - monkeypatch.setenv('COLUMNS', '42') - assert terminalwriter.get_terminal_width() == 42 - monkeypatch.delenv('COLUMNS', raising=False) - -def test_terminalwriter_defaultwidth_80(monkeypatch): - monkeypatch.setattr(terminalwriter, '_getdimensions', lambda: 0/0) - monkeypatch.delenv('COLUMNS', raising=False) - tw = py.io.TerminalWriter() - assert tw.fullwidth == 80 - -def test_terminalwriter_getdimensions_bogus(monkeypatch): - monkeypatch.setattr(terminalwriter, '_getdimensions', lambda: (10,10)) - monkeypatch.delenv('COLUMNS', raising=False) - tw = py.io.TerminalWriter() - assert tw.fullwidth == 80 - -def test_terminalwriter_getdimensions_emacs(monkeypatch): - # emacs terminal returns (0,0) but set COLUMNS properly - monkeypatch.setattr(terminalwriter, '_getdimensions', lambda: (0,0)) - monkeypatch.setenv('COLUMNS', '42') - tw = py.io.TerminalWriter() - assert tw.fullwidth == 42 - -def test_terminalwriter_computes_width(monkeypatch): - monkeypatch.setattr(terminalwriter, 'get_terminal_width', lambda: 42) - tw = py.io.TerminalWriter() - assert tw.fullwidth == 42 - -def test_terminalwriter_default_instantiation(): - tw = py.io.TerminalWriter(stringio=True) - assert hasattr(tw, 'stringio') - -def test_terminalwriter_dumb_term_no_markup(monkeypatch): - monkeypatch.setattr(os, 'environ', {'TERM': 'dumb', 'PATH': ''}) - class MyFile: - closed = False - def isatty(self): - return True - monkeypatch.setattr(sys, 'stdout', MyFile()) - try: - assert sys.stdout.isatty() - tw = py.io.TerminalWriter() - assert not tw.hasmarkup - finally: - monkeypatch.undo() - -def test_terminalwriter_file_unicode(tmpdir): - f = codecs.open(str(tmpdir.join("xyz")), "wb", "utf8") - tw = py.io.TerminalWriter(file=f) - assert tw.encoding == "utf8" - -def test_unicode_encoding(): - msg = py.builtin._totext('b\u00f6y', 'utf8') - for encoding in 'utf8', 'latin1': - l = [] - tw = py.io.TerminalWriter(l.append, encoding=encoding) - tw.line(msg) - assert l[0].strip() == msg.encode(encoding) - -@pytest.mark.parametrize("encoding", ["ascii"]) -def test_unicode_on_file_with_ascii_encoding(tmpdir, monkeypatch, encoding): - msg = py.builtin._totext('hell\xf6', "latin1") - #pytest.raises(UnicodeEncodeError, lambda: bytes(msg)) - f = codecs.open(str(tmpdir.join("x")), "w", encoding) - tw = py.io.TerminalWriter(f) - tw.line(msg) - f.close() - s = tmpdir.join("x").open("rb").read().strip() - assert encoding == "ascii" - assert s == msg.encode("unicode-escape") - - -win32 = int(sys.platform == "win32") -class TestTerminalWriter: - def pytest_generate_tests(self, metafunc): - if "tw" in metafunc.funcargnames: - metafunc.addcall(id="path", param="path") - metafunc.addcall(id="stringio", param="stringio") - metafunc.addcall(id="callable", param="callable") - def pytest_funcarg__tw(self, request): - if request.param == "path": - tmpdir = request.getfuncargvalue("tmpdir") - p = tmpdir.join("tmpfile") - f = codecs.open(str(p), 'w+', encoding='utf8') - tw = py.io.TerminalWriter(f) - def getlines(): - tw._file.flush() - return codecs.open(str(p), 'r', - encoding='utf8').readlines() - elif request.param == "stringio": - tw = py.io.TerminalWriter(stringio=True) - def getlines(): - tw.stringio.seek(0) - return tw.stringio.readlines() - elif request.param == "callable": - writes = [] - tw = py.io.TerminalWriter(writes.append) - def getlines(): - io = py.io.TextIO() - io.write("".join(writes)) - io.seek(0) - return io.readlines() - tw.getlines = getlines - tw.getvalue = lambda: "".join(getlines()) - return tw - - def test_line(self, tw): - tw.line("hello") - l = tw.getlines() - assert len(l) == 1 - assert l[0] == "hello\n" - - def test_line_unicode(self, tw): - for encoding in 'utf8', 'latin1': - tw._encoding = encoding - msg = py.builtin._totext('b\u00f6y', 'utf8') - tw.line(msg) - l = tw.getlines() - assert l[0] == msg + "\n" - - def test_sep_no_title(self, tw): - tw.sep("-", fullwidth=60) - l = tw.getlines() - assert len(l) == 1 - assert l[0] == "-" * (60-win32) + "\n" - - def test_sep_with_title(self, tw): - tw.sep("-", "hello", fullwidth=60) - l = tw.getlines() - assert len(l) == 1 - assert l[0] == "-" * 26 + " hello " + "-" * (27-win32) + "\n" - - def test_sep_longer_than_width(self, tw): - tw.sep('-', 'a' * 10, fullwidth=5) - line, = tw.getlines() - # even though the string is wider than the line, still have a separator - assert line == '- aaaaaaaaaa -\n' - - @py.test.mark.skipif("sys.platform == 'win32'") - def test__escaped(self, tw): - text2 = tw._escaped("hello", (31)) - assert text2.find("hello") != -1 - - @py.test.mark.skipif("sys.platform == 'win32'") - def test_markup(self, tw): - for bold in (True, False): - for color in ("red", "green"): - text2 = tw.markup("hello", **{color: True, 'bold': bold}) - assert text2.find("hello") != -1 - py.test.raises(ValueError, "tw.markup('x', wronkw=3)") - py.test.raises(ValueError, "tw.markup('x', wronkw=0)") - - def test_line_write_markup(self, tw): - tw.hasmarkup = True - tw.line("x", bold=True) - tw.write("x\n", red=True) - l = tw.getlines() - if sys.platform != "win32": - assert len(l[0]) >= 2, l - assert len(l[1]) >= 2, l - - def test_attr_fullwidth(self, tw): - tw.sep("-", "hello", fullwidth=70) - tw.fullwidth = 70 - tw.sep("-", "hello") - l = tw.getlines() - assert len(l[0]) == len(l[1]) - - def test_reline(self, tw): - tw.line("hello") - tw.hasmarkup = False - pytest.raises(ValueError, lambda: tw.reline("x")) - tw.hasmarkup = True - tw.reline("0 1 2") - tw.getlines() - l = tw.getvalue().split("\n") - assert len(l) == 2 - tw.reline("0 1 3") - l = tw.getvalue().split("\n") - assert len(l) == 2 - assert l[1].endswith("0 1 3\r") - tw.line("so") - l = tw.getvalue().split("\n") - assert len(l) == 3 - assert l[-1] == "" - assert l[1] == ("0 1 2\r0 1 3\rso ") - assert l[0] == "hello" - - -def test_terminal_with_callable_write_and_flush(): - l = set() - class fil: - flush = lambda self: l.add("1") - write = lambda self, x: l.add("1") - __call__ = lambda self, x: l.add("2") - - tw = py.io.TerminalWriter(fil()) - tw.line("hello") - assert l == set(["1"]) - del fil.flush - l.clear() - tw = py.io.TerminalWriter(fil()) - tw.line("hello") - assert l == set(["2"]) - - -def test_chars_on_current_line(): - tw = py.io.TerminalWriter(stringio=True) - - written = [] - - def write_and_check(s, expected): - tw.write(s, bold=True) - written.append(s) - assert tw.chars_on_current_line == expected - assert tw.stringio.getvalue() == ''.join(written) - - write_and_check('foo', 3) - write_and_check('bar', 6) - write_and_check('\n', 0) - write_and_check('\n', 0) - write_and_check('\n\n\n', 0) - write_and_check('\nfoo', 3) - write_and_check('\nfbar\nhello', 5) - write_and_check('10', 7) - - -@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") -def test_attr_hasmarkup(): - tw = py.io.TerminalWriter(stringio=True) - assert not tw.hasmarkup - tw.hasmarkup = True - tw.line("hello", bold=True) - s = tw.stringio.getvalue() - assert len(s) > len("hello\n") - assert '\x1b[1m' in s - assert '\x1b[0m' in s - -@pytest.mark.skipif(sys.platform == "win32", reason="win32 has no native ansi") -def test_ansi_print(): - # we have no easy way to construct a file that - # represents a terminal - f = py.io.TextIO() - f.isatty = lambda: True - py.io.ansi_print("hello", 0x32, file=f) - text2 = f.getvalue() - assert text2.find("hello") != -1 - assert len(text2) >= len("hello\n") - assert '\x1b[50m' in text2 - assert '\x1b[0m' in text2 - -def test_should_do_markup_PY_COLORS_eq_1(monkeypatch): - monkeypatch.setitem(os.environ, 'PY_COLORS', '1') - tw = py.io.TerminalWriter(stringio=True) - assert tw.hasmarkup - tw.line("hello", bold=True) - s = tw.stringio.getvalue() - assert len(s) > len("hello\n") - assert '\x1b[1m' in s - assert '\x1b[0m' in s - -def test_should_do_markup_PY_COLORS_eq_0(monkeypatch): - monkeypatch.setitem(os.environ, 'PY_COLORS', '0') - f = py.io.TextIO() - f.isatty = lambda: True - tw = py.io.TerminalWriter(file=f) - assert not tw.hasmarkup - tw.line("hello", bold=True) - s = f.getvalue() - assert s == "hello\n" - -def test_should_do_markup(monkeypatch): - monkeypatch.delenv("PY_COLORS", raising=False) - monkeypatch.delenv("NO_COLOR", raising=False) - - should_do_markup = terminalwriter.should_do_markup - - f = py.io.TextIO() - f.isatty = lambda: True - - assert should_do_markup(f) is True - - # NO_COLOR without PY_COLORS. - monkeypatch.setenv("NO_COLOR", "0") - assert should_do_markup(f) is False - monkeypatch.setenv("NO_COLOR", "1") - assert should_do_markup(f) is False - monkeypatch.setenv("NO_COLOR", "any") - assert should_do_markup(f) is False - - # PY_COLORS overrides NO_COLOR ("0" and "1" only). - monkeypatch.setenv("PY_COLORS", "1") - assert should_do_markup(f) is True - monkeypatch.setenv("PY_COLORS", "0") - assert should_do_markup(f) is False - # Uses NO_COLOR. - monkeypatch.setenv("PY_COLORS", "any") - assert should_do_markup(f) is False - monkeypatch.delenv("NO_COLOR") - assert should_do_markup(f) is True - - # Back to defaults. - monkeypatch.delenv("PY_COLORS") - assert should_do_markup(f) is True - f.isatty = lambda: False - assert should_do_markup(f) is False diff --git a/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py b/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py deleted file mode 100644 index e6d84fbf7a1..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/io_/test_terminalwriter_linewidth.py +++ /dev/null @@ -1,56 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -from py._io.terminalwriter import TerminalWriter - - -def test_terminal_writer_line_width_init(): - tw = TerminalWriter() - assert tw.chars_on_current_line == 0 - assert tw.width_of_current_line == 0 - - -def test_terminal_writer_line_width_update(): - tw = TerminalWriter() - tw.write('hello world') - assert tw.chars_on_current_line == 11 - assert tw.width_of_current_line == 11 - - -def test_terminal_writer_line_width_update_with_newline(): - tw = TerminalWriter() - tw.write('hello\nworld') - assert tw.chars_on_current_line == 5 - assert tw.width_of_current_line == 5 - - -def test_terminal_writer_line_width_update_with_wide_text(): - tw = TerminalWriter() - tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚') - assert tw.chars_on_current_line == 11 - assert tw.width_of_current_line == 21 # 5*2 + 1 + 5*2 - - -def test_terminal_writer_line_width_update_with_wide_bytes(): - tw = TerminalWriter() - tw.write('乇乂ㄒ尺卂 ㄒ卄丨匚匚'.encode('utf-8')) - assert tw.chars_on_current_line == 11 - assert tw.width_of_current_line == 21 - - -def test_terminal_writer_line_width_composed(): - tw = TerminalWriter() - text = 'café food' - assert len(text) == 9 - tw.write(text) - assert tw.chars_on_current_line == 9 - assert tw.width_of_current_line == 9 - - -def test_terminal_writer_line_width_combining(): - tw = TerminalWriter() - text = 'café food' - assert len(text) == 10 - tw.write(text) - assert tw.chars_on_current_line == 10 - assert tw.width_of_current_line == 9 diff --git a/tests/wpt/tests/tools/third_party/py/testing/log/__init__.py b/tests/wpt/tests/tools/third_party/py/testing/log/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/log/__init__.py +++ /dev/null diff --git a/tests/wpt/tests/tools/third_party/py/testing/log/test_log.py b/tests/wpt/tests/tools/third_party/py/testing/log/test_log.py deleted file mode 100644 index 5c706d9b6ad..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/log/test_log.py +++ /dev/null @@ -1,191 +0,0 @@ -import py - -from py._log.log import default_keywordmapper - -callcapture = py.io.StdCapture.call - - -def setup_module(mod): - mod._oldstate = default_keywordmapper.getstate() - -def teardown_module(mod): - default_keywordmapper.setstate(mod._oldstate) - -class TestLogProducer: - def setup_method(self, meth): - from py._log.log import default_keywordmapper - default_keywordmapper.setstate(_oldstate) - - def test_getstate_setstate(self): - state = py.log._getstate() - py.log.setconsumer("hello", [].append) - state2 = py.log._getstate() - assert state2 != state - py.log._setstate(state) - state3 = py.log._getstate() - assert state3 == state - - def test_producer_repr(self): - d = py.log.Producer("default") - assert repr(d).find('default') != -1 - - def test_produce_one_keyword(self): - l = [] - py.log.setconsumer('s1', l.append) - py.log.Producer('s1')("hello world") - assert len(l) == 1 - msg = l[0] - assert msg.content().startswith('hello world') - assert msg.prefix() == '[s1] ' - assert str(msg) == "[s1] hello world" - - def test_producer_class(self): - p = py.log.Producer('x1') - l = [] - py.log.setconsumer(p._keywords, l.append) - p("hello") - assert len(l) == 1 - assert len(l[0].keywords) == 1 - assert 'x1' == l[0].keywords[0] - - def test_producer_caching(self): - p = py.log.Producer('x1') - x2 = p.x2 - assert x2 is p.x2 - -class TestLogConsumer: - def setup_method(self, meth): - default_keywordmapper.setstate(_oldstate) - def test_log_none(self): - log = py.log.Producer("XXX") - l = [] - py.log.setconsumer('XXX', l.append) - log("1") - assert l - l[:] = [] - py.log.setconsumer('XXX', None) - log("2") - assert not l - - def test_log_default_stderr(self): - res, out, err = callcapture(py.log.Producer("default"), "hello") - assert err.strip() == "[default] hello" - - def test_simple_consumer_match(self): - l = [] - py.log.setconsumer("x1", l.append) - p = py.log.Producer("x1 x2") - p("hello") - assert l - assert l[0].content() == "hello" - - def test_simple_consumer_match_2(self): - l = [] - p = py.log.Producer("x1 x2") - py.log.setconsumer(p._keywords, l.append) - p("42") - assert l - assert l[0].content() == "42" - - def test_no_auto_producer(self): - p = py.log.Producer('x') - py.test.raises(AttributeError, "p._x") - py.test.raises(AttributeError, "p.x_y") - - def test_setconsumer_with_producer(self): - l = [] - p = py.log.Producer("hello") - py.log.setconsumer(p, l.append) - p("world") - assert str(l[0]) == "[hello] world" - - def test_multi_consumer(self): - l = [] - py.log.setconsumer("x1", l.append) - py.log.setconsumer("x1 x2", None) - p = py.log.Producer("x1 x2") - p("hello") - assert not l - py.log.Producer("x1")("hello") - assert l - assert l[0].content() == "hello" - - def test_log_stderr(self): - py.log.setconsumer("xyz", py.log.STDOUT) - res, out, err = callcapture(py.log.Producer("xyz"), "hello") - assert not err - assert out.strip() == '[xyz] hello' - - def test_log_file(self, tmpdir): - customlog = tmpdir.join('log.out') - py.log.setconsumer("default", open(str(customlog), 'w', 1)) - py.log.Producer("default")("hello world #1") - assert customlog.readlines() == ['[default] hello world #1\n'] - - py.log.setconsumer("default", py.log.Path(customlog, buffering=False)) - py.log.Producer("default")("hello world #2") - res = customlog.readlines() - assert res == ['[default] hello world #2\n'] # no append by default! - - def test_log_file_append_mode(self, tmpdir): - logfilefn = tmpdir.join('log_append.out') - - # The append mode is on by default, so we don't need to specify it for File - py.log.setconsumer("default", py.log.Path(logfilefn, append=True, - buffering=0)) - assert logfilefn.check() - py.log.Producer("default")("hello world #1") - lines = logfilefn.readlines() - assert lines == ['[default] hello world #1\n'] - py.log.setconsumer("default", py.log.Path(logfilefn, append=True, - buffering=0)) - py.log.Producer("default")("hello world #1") - lines = logfilefn.readlines() - assert lines == ['[default] hello world #1\n', - '[default] hello world #1\n'] - - def test_log_file_delayed_create(self, tmpdir): - logfilefn = tmpdir.join('log_create.out') - - py.log.setconsumer("default", py.log.Path(logfilefn, - delayed_create=True, buffering=0)) - assert not logfilefn.check() - py.log.Producer("default")("hello world #1") - lines = logfilefn.readlines() - assert lines == ['[default] hello world #1\n'] - - def test_keyword_based_log_files(self, tmpdir): - logfiles = [] - keywords = 'k1 k2 k3'.split() - for key in keywords: - path = tmpdir.join(key) - py.log.setconsumer(key, py.log.Path(path, buffering=0)) - - py.log.Producer('k1')('1') - py.log.Producer('k2')('2') - py.log.Producer('k3')('3') - - for key in keywords: - path = tmpdir.join(key) - assert path.read().strip() == '[%s] %s' % (key, key[-1]) - - # disabled for now; the syslog log file can usually be read only by root - # I manually inspected /var/log/messages and the entries were there - def no_test_log_syslog(self): - py.log.setconsumer("default", py.log.Syslog()) - py.log.default("hello world #1") - - # disabled for now until I figure out how to read entries in the - # Event Logs on Windows - # I manually inspected the Application Log and the entries were there - def no_test_log_winevent(self): - py.log.setconsumer("default", py.log.WinEvent()) - py.log.default("hello world #1") - - # disabled for now until I figure out how to properly pass the parameters - def no_test_log_email(self): - py.log.setconsumer("default", py.log.Email(mailhost="gheorghiu.net", - fromaddr="grig", - toaddrs="grig", - subject = "py.log email")) - py.log.default("hello world #1") diff --git a/tests/wpt/tests/tools/third_party/py/testing/log/test_warning.py b/tests/wpt/tests/tools/third_party/py/testing/log/test_warning.py deleted file mode 100644 index 36efec913a3..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/log/test_warning.py +++ /dev/null @@ -1,85 +0,0 @@ -import sys -from distutils.version import LooseVersion - -import pytest - -import py - -mypath = py.path.local(__file__).new(ext=".py") - - -pytestmark = pytest.mark.skipif(LooseVersion(pytest.__version__) >= LooseVersion('3.1'), - reason='apiwarn is not compatible with pytest >= 3.1 (#162)') - - -@pytest.mark.xfail -def test_forwarding_to_warnings_module(): - pytest.deprecated_call(py.log._apiwarn, "1.3", "..") - -def test_apiwarn_functional(recwarn): - capture = py.io.StdCapture() - py.log._apiwarn("x.y.z", "something", stacklevel=1) - out, err = capture.reset() - py.builtin.print_("out", out) - py.builtin.print_("err", err) - assert err.find("x.y.z") != -1 - lno = py.code.getrawcode(test_apiwarn_functional).co_firstlineno + 2 - exp = "%s:%s" % (mypath, lno) - assert err.find(exp) != -1 - -def test_stacklevel(recwarn): - def f(): - py.log._apiwarn("x", "some", stacklevel=2) - # 3 - # 4 - capture = py.io.StdCapture() - f() - out, err = capture.reset() - lno = py.code.getrawcode(test_stacklevel).co_firstlineno + 6 - warning = str(err) - assert warning.find(":%s" % lno) != -1 - -def test_stacklevel_initpkg_with_resolve(testdir, recwarn): - testdir.makepyfile(modabc=""" - import py - def f(): - py.log._apiwarn("x", "some", stacklevel="apipkg123") - """) - testdir.makepyfile(apipkg123=""" - def __getattr__(): - import modabc - modabc.f() - """) - p = testdir.makepyfile(""" - import apipkg123 - apipkg123.__getattr__() - """) - capture = py.io.StdCapture() - p.pyimport() - out, err = capture.reset() - warning = str(err) - loc = 'test_stacklevel_initpkg_with_resolve.py:2' - assert warning.find(loc) != -1 - -def test_stacklevel_initpkg_no_resolve(recwarn): - def f(): - py.log._apiwarn("x", "some", stacklevel="apipkg") - capture = py.io.StdCapture() - f() - out, err = capture.reset() - lno = py.code.getrawcode(test_stacklevel_initpkg_no_resolve).co_firstlineno + 2 - warning = str(err) - assert warning.find(":%s" % lno) != -1 - - -def test_function(recwarn): - capture = py.io.StdCapture() - py.log._apiwarn("x.y.z", "something", function=test_function) - out, err = capture.reset() - py.builtin.print_("out", out) - py.builtin.print_("err", err) - assert err.find("x.y.z") != -1 - lno = py.code.getrawcode(test_function).co_firstlineno - exp = "%s:%s" % (mypath, lno) - assert err.find(exp) != -1 - diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/common.py b/tests/wpt/tests/tools/third_party/py/testing/path/common.py deleted file mode 100644 index d69a1c39d09..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/common.py +++ /dev/null @@ -1,492 +0,0 @@ -import py -import sys - -import pytest - -class CommonFSTests(object): - def test_constructor_equality(self, path1): - p = path1.__class__(path1) - assert p == path1 - - def test_eq_nonstring(self, path1): - p1 = path1.join('sampledir') - p2 = path1.join('sampledir') - assert p1 == p2 - - def test_new_identical(self, path1): - assert path1 == path1.new() - - def test_join(self, path1): - p = path1.join('sampledir') - strp = str(p) - assert strp.endswith('sampledir') - assert strp.startswith(str(path1)) - - def test_join_normalized(self, path1): - newpath = path1.join(path1.sep+'sampledir') - strp = str(newpath) - assert strp.endswith('sampledir') - assert strp.startswith(str(path1)) - newpath = path1.join((path1.sep*2) + 'sampledir') - strp = str(newpath) - assert strp.endswith('sampledir') - assert strp.startswith(str(path1)) - - def test_join_noargs(self, path1): - newpath = path1.join() - assert path1 == newpath - - def test_add_something(self, path1): - p = path1.join('sample') - p = p + 'dir' - assert p.check() - assert p.exists() - assert p.isdir() - assert not p.isfile() - - def test_parts(self, path1): - newpath = path1.join('sampledir', 'otherfile') - par = newpath.parts()[-3:] - assert par == [path1, path1.join('sampledir'), newpath] - - revpar = newpath.parts(reverse=True)[:3] - assert revpar == [newpath, path1.join('sampledir'), path1] - - def test_common(self, path1): - other = path1.join('sampledir') - x = other.common(path1) - assert x == path1 - - #def test_parents_nonexisting_file(self, path1): - # newpath = path1 / 'dirnoexist' / 'nonexisting file' - # par = list(newpath.parents()) - # assert par[:2] == [path1 / 'dirnoexist', path1] - - def test_basename_checks(self, path1): - newpath = path1.join('sampledir') - assert newpath.check(basename='sampledir') - assert newpath.check(notbasename='xyz') - assert newpath.basename == 'sampledir' - - def test_basename(self, path1): - newpath = path1.join('sampledir') - assert newpath.check(basename='sampledir') - assert newpath.basename, 'sampledir' - - def test_dirname(self, path1): - newpath = path1.join('sampledir') - assert newpath.dirname == str(path1) - - def test_dirpath(self, path1): - newpath = path1.join('sampledir') - assert newpath.dirpath() == path1 - - def test_dirpath_with_args(self, path1): - newpath = path1.join('sampledir') - assert newpath.dirpath('x') == path1.join('x') - - def test_newbasename(self, path1): - newpath = path1.join('samplefile') - newbase = newpath.new(basename="samplefile2") - assert newbase.basename == "samplefile2" - assert newbase.dirpath() == newpath.dirpath() - - def test_not_exists(self, path1): - assert not path1.join('does_not_exist').check() - assert path1.join('does_not_exist').check(exists=0) - - def test_exists(self, path1): - assert path1.join("samplefile").check() - assert path1.join("samplefile").check(exists=1) - assert path1.join("samplefile").exists() - assert path1.join("samplefile").isfile() - assert not path1.join("samplefile").isdir() - - def test_dir(self, path1): - #print repr(path1.join("sampledir")) - assert path1.join("sampledir").check(dir=1) - assert path1.join('samplefile').check(notdir=1) - assert not path1.join("samplefile").check(dir=1) - assert path1.join("samplefile").exists() - assert not path1.join("samplefile").isdir() - assert path1.join("samplefile").isfile() - - def test_fnmatch_file(self, path1): - assert path1.join("samplefile").check(fnmatch='s*e') - assert path1.join("samplefile").fnmatch('s*e') - assert not path1.join("samplefile").fnmatch('s*x') - assert not path1.join("samplefile").check(fnmatch='s*x') - - #def test_fnmatch_dir(self, path1): - - # pattern = path1.sep.join(['s*file']) - # sfile = path1.join("samplefile") - # assert sfile.check(fnmatch=pattern) - - def test_relto(self, path1): - l=path1.join("sampledir", "otherfile") - assert l.relto(path1) == l.sep.join(["sampledir", "otherfile"]) - assert l.check(relto=path1) - assert path1.check(notrelto=l) - assert not path1.check(relto=l) - - def test_bestrelpath(self, path1): - curdir = path1 - sep = curdir.sep - s = curdir.bestrelpath(curdir) - assert s == "." - s = curdir.bestrelpath(curdir.join("hello", "world")) - assert s == "hello" + sep + "world" - - s = curdir.bestrelpath(curdir.dirpath().join("sister")) - assert s == ".." + sep + "sister" - assert curdir.bestrelpath(curdir.dirpath()) == ".." - - assert curdir.bestrelpath("hello") == "hello" - - def test_relto_not_relative(self, path1): - l1=path1.join("bcde") - l2=path1.join("b") - assert not l1.relto(l2) - assert not l2.relto(l1) - - @py.test.mark.xfail("sys.platform.startswith('java')") - def test_listdir(self, path1): - l = path1.listdir() - assert path1.join('sampledir') in l - assert path1.join('samplefile') in l - py.test.raises(py.error.ENOTDIR, - "path1.join('samplefile').listdir()") - - def test_listdir_fnmatchstring(self, path1): - l = path1.listdir('s*dir') - assert len(l) - assert l[0], path1.join('sampledir') - - def test_listdir_filter(self, path1): - l = path1.listdir(lambda x: x.check(dir=1)) - assert path1.join('sampledir') in l - assert not path1.join('samplefile') in l - - def test_listdir_sorted(self, path1): - l = path1.listdir(lambda x: x.check(basestarts="sample"), sort=True) - assert path1.join('sampledir') == l[0] - assert path1.join('samplefile') == l[1] - assert path1.join('samplepickle') == l[2] - - def test_visit_nofilter(self, path1): - l = [] - for i in path1.visit(): - l.append(i.relto(path1)) - assert "sampledir" in l - assert path1.sep.join(["sampledir", "otherfile"]) in l - - def test_visit_norecurse(self, path1): - l = [] - for i in path1.visit(None, lambda x: x.basename != "sampledir"): - l.append(i.relto(path1)) - assert "sampledir" in l - assert not path1.sep.join(["sampledir", "otherfile"]) in l - - @pytest.mark.parametrize('fil', ['*dir', u'*dir', - pytest.mark.skip("sys.version_info <" - " (3,6)")(b'*dir')]) - def test_visit_filterfunc_is_string(self, path1, fil): - l = [] - for i in path1.visit(fil): - l.append(i.relto(path1)) - assert len(l), 2 - assert "sampledir" in l - assert "otherdir" in l - - @py.test.mark.xfail("sys.platform.startswith('java')") - def test_visit_ignore(self, path1): - p = path1.join('nonexisting') - assert list(p.visit(ignore=py.error.ENOENT)) == [] - - def test_visit_endswith(self, path1): - l = [] - for i in path1.visit(lambda x: x.check(endswith="file")): - l.append(i.relto(path1)) - assert path1.sep.join(["sampledir", "otherfile"]) in l - assert "samplefile" in l - - def test_endswith(self, path1): - assert path1.check(notendswith='.py') - x = path1.join('samplefile') - assert x.check(endswith='file') - - def test_cmp(self, path1): - path1 = path1.join('samplefile') - path2 = path1.join('samplefile2') - assert (path1 < path2) == ('samplefile' < 'samplefile2') - assert not (path1 < path1) - - def test_simple_read(self, path1): - x = path1.join('samplefile').read('r') - assert x == 'samplefile\n' - - def test_join_div_operator(self, path1): - newpath = path1 / '/sampledir' / '/test//' - newpath2 = path1.join('sampledir', 'test') - assert newpath == newpath2 - - def test_ext(self, path1): - newpath = path1.join('sampledir.ext') - assert newpath.ext == '.ext' - newpath = path1.join('sampledir') - assert not newpath.ext - - def test_purebasename(self, path1): - newpath = path1.join('samplefile.py') - assert newpath.purebasename == 'samplefile' - - def test_multiple_parts(self, path1): - newpath = path1.join('samplefile.py') - dirname, purebasename, basename, ext = newpath._getbyspec( - 'dirname,purebasename,basename,ext') - assert str(path1).endswith(dirname) # be careful with win32 'drive' - assert purebasename == 'samplefile' - assert basename == 'samplefile.py' - assert ext == '.py' - - def test_dotted_name_ext(self, path1): - newpath = path1.join('a.b.c') - ext = newpath.ext - assert ext == '.c' - assert newpath.ext == '.c' - - def test_newext(self, path1): - newpath = path1.join('samplefile.py') - newext = newpath.new(ext='.txt') - assert newext.basename == "samplefile.txt" - assert newext.purebasename == "samplefile" - - def test_readlines(self, path1): - fn = path1.join('samplefile') - contents = fn.readlines() - assert contents == ['samplefile\n'] - - def test_readlines_nocr(self, path1): - fn = path1.join('samplefile') - contents = fn.readlines(cr=0) - assert contents == ['samplefile', ''] - - def test_file(self, path1): - assert path1.join('samplefile').check(file=1) - - def test_not_file(self, path1): - assert not path1.join("sampledir").check(file=1) - assert path1.join("sampledir").check(file=0) - - def test_non_existent(self, path1): - assert path1.join("sampledir.nothere").check(dir=0) - assert path1.join("sampledir.nothere").check(file=0) - assert path1.join("sampledir.nothere").check(notfile=1) - assert path1.join("sampledir.nothere").check(notdir=1) - assert path1.join("sampledir.nothere").check(notexists=1) - assert not path1.join("sampledir.nothere").check(notfile=0) - - # pattern = path1.sep.join(['s*file']) - # sfile = path1.join("samplefile") - # assert sfile.check(fnmatch=pattern) - - def test_size(self, path1): - url = path1.join("samplefile") - assert url.size() > len("samplefile") - - def test_mtime(self, path1): - url = path1.join("samplefile") - assert url.mtime() > 0 - - def test_relto_wrong_type(self, path1): - py.test.raises(TypeError, "path1.relto(42)") - - def test_load(self, path1): - p = path1.join('samplepickle') - obj = p.load() - assert type(obj) is dict - assert obj.get('answer',None) == 42 - - def test_visit_filesonly(self, path1): - l = [] - for i in path1.visit(lambda x: x.check(file=1)): - l.append(i.relto(path1)) - assert not "sampledir" in l - assert path1.sep.join(["sampledir", "otherfile"]) in l - - def test_visit_nodotfiles(self, path1): - l = [] - for i in path1.visit(lambda x: x.check(dotfile=0)): - l.append(i.relto(path1)) - assert "sampledir" in l - assert path1.sep.join(["sampledir", "otherfile"]) in l - assert not ".dotfile" in l - - def test_visit_breadthfirst(self, path1): - l = [] - for i in path1.visit(bf=True): - l.append(i.relto(path1)) - for i, p in enumerate(l): - if path1.sep in p: - for j in range(i, len(l)): - assert path1.sep in l[j] - break - else: - py.test.fail("huh") - - def test_visit_sort(self, path1): - l = [] - for i in path1.visit(bf=True, sort=True): - l.append(i.relto(path1)) - for i, p in enumerate(l): - if path1.sep in p: - break - assert l[:i] == sorted(l[:i]) - assert l[i:] == sorted(l[i:]) - - def test_endswith(self, path1): - def chk(p): - return p.check(endswith="pickle") - assert not chk(path1) - assert not chk(path1.join('samplefile')) - assert chk(path1.join('somepickle')) - - def test_copy_file(self, path1): - otherdir = path1.join('otherdir') - initpy = otherdir.join('__init__.py') - copied = otherdir.join('copied') - initpy.copy(copied) - try: - assert copied.check() - s1 = initpy.read() - s2 = copied.read() - assert s1 == s2 - finally: - if copied.check(): - copied.remove() - - def test_copy_dir(self, path1): - otherdir = path1.join('otherdir') - copied = path1.join('newdir') - try: - otherdir.copy(copied) - assert copied.check(dir=1) - assert copied.join('__init__.py').check(file=1) - s1 = otherdir.join('__init__.py').read() - s2 = copied.join('__init__.py').read() - assert s1 == s2 - finally: - if copied.check(dir=1): - copied.remove(rec=1) - - def test_remove_file(self, path1): - d = path1.ensure('todeleted') - assert d.check() - d.remove() - assert not d.check() - - def test_remove_dir_recursive_by_default(self, path1): - d = path1.ensure('to', 'be', 'deleted') - assert d.check() - p = path1.join('to') - p.remove() - assert not p.check() - - def test_ensure_dir(self, path1): - b = path1.ensure_dir("001", "002") - assert b.basename == "002" - assert b.isdir() - - def test_mkdir_and_remove(self, path1): - tmpdir = path1 - py.test.raises(py.error.EEXIST, tmpdir.mkdir, 'sampledir') - new = tmpdir.join('mktest1') - new.mkdir() - assert new.check(dir=1) - new.remove() - - new = tmpdir.mkdir('mktest') - assert new.check(dir=1) - new.remove() - assert tmpdir.join('mktest') == new - - def test_move_file(self, path1): - p = path1.join('samplefile') - newp = p.dirpath('moved_samplefile') - p.move(newp) - try: - assert newp.check(file=1) - assert not p.check() - finally: - dp = newp.dirpath() - if hasattr(dp, 'revert'): - dp.revert() - else: - newp.move(p) - assert p.check() - - def test_move_dir(self, path1): - source = path1.join('sampledir') - dest = path1.join('moveddir') - source.move(dest) - assert dest.check(dir=1) - assert dest.join('otherfile').check(file=1) - assert not source.join('sampledir').check() - - def test_fspath_protocol_match_strpath(self, path1): - assert path1.__fspath__() == path1.strpath - - def test_fspath_func_match_strpath(self, path1): - try: - from os import fspath - except ImportError: - from py._path.common import fspath - assert fspath(path1) == path1.strpath - - @py.test.mark.skip("sys.version_info < (3,6)") - def test_fspath_open(self, path1): - f = path1.join('opentestfile') - open(f) - - @py.test.mark.skip("sys.version_info < (3,6)") - def test_fspath_fsencode(self, path1): - from os import fsencode - assert fsencode(path1) == fsencode(path1.strpath) - -def setuptestfs(path): - if path.join('samplefile').check(): - return - #print "setting up test fs for", repr(path) - samplefile = path.ensure('samplefile') - samplefile.write('samplefile\n') - - execfile = path.ensure('execfile') - execfile.write('x=42') - - execfilepy = path.ensure('execfile.py') - execfilepy.write('x=42') - - d = {1:2, 'hello': 'world', 'answer': 42} - path.ensure('samplepickle').dump(d) - - sampledir = path.ensure('sampledir', dir=1) - sampledir.ensure('otherfile') - - otherdir = path.ensure('otherdir', dir=1) - otherdir.ensure('__init__.py') - - module_a = otherdir.ensure('a.py') - module_a.write('from .b import stuff as result\n') - module_b = otherdir.ensure('b.py') - module_b.write('stuff="got it"\n') - module_c = otherdir.ensure('c.py') - module_c.write('''import py; -import otherdir.a -value = otherdir.a.result -''') - module_d = otherdir.ensure('d.py') - module_d.write('''import py; -from otherdir import a -value2 = a.result -''') diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/conftest.py b/tests/wpt/tests/tools/third_party/py/testing/path/conftest.py deleted file mode 100644 index 84fb5c82694..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/conftest.py +++ /dev/null @@ -1,80 +0,0 @@ -import py -import sys -from py._path import svnwc as svncommon - -svnbin = py.path.local.sysfind('svn') -repodump = py.path.local(__file__).dirpath('repotest.dump') -from py.builtin import print_ - -def pytest_funcarg__repowc1(request): - if svnbin is None: - py.test.skip("svn binary not found") - - tmpdir = request.getfuncargvalue("tmpdir") - repo, repourl, wc = request.cached_setup( - setup=lambda: getrepowc(tmpdir, "path1repo", "path1wc"), - scope="module", - ) - for x in ('test_remove', 'test_move', 'test_status_deleted'): - if request.function.__name__.startswith(x): - #print >>sys.stderr, ("saving repo", repo, "for", request.function) - _savedrepowc = save_repowc(repo, wc) - request.addfinalizer(lambda: restore_repowc(_savedrepowc)) - return repo, repourl, wc - -def pytest_funcarg__repowc2(request): - tmpdir = request.getfuncargvalue("tmpdir") - name = request.function.__name__ - repo, url, wc = getrepowc(tmpdir, "%s-repo-2" % name, "%s-wc-2" % name) - return repo, url, wc - -def getsvnbin(): - if svnbin is None: - py.test.skip("svn binary not found") - return svnbin - -# make a wc directory out of a given root url -# cache previously obtained wcs! -# -def getrepowc(tmpdir, reponame='basetestrepo', wcname='wc'): - repo = tmpdir.mkdir(reponame) - wcdir = tmpdir.mkdir(wcname) - repo.ensure(dir=1) - py.process.cmdexec('svnadmin create "%s"' % - svncommon._escape_helper(repo)) - py.process.cmdexec('svnadmin load -q "%s" <"%s"' % - (svncommon._escape_helper(repo), repodump)) - print_("created svn repository", repo) - wcdir.ensure(dir=1) - wc = py.path.svnwc(wcdir) - if sys.platform == 'win32': - repourl = "file://" + '/' + str(repo).replace('\\', '/') - else: - repourl = "file://%s" % repo - wc.checkout(repourl) - print_("checked out new repo into", wc) - return (repo, repourl, wc) - - -def save_repowc(repo, wc): - assert not str(repo).startswith("file://"), repo - assert repo.check() - savedrepo = repo.dirpath(repo.basename+".1") - savedwc = wc.dirpath(wc.basename+".1") - repo.copy(savedrepo) - wc.localpath.copy(savedwc.localpath) - return savedrepo, savedwc - -def restore_repowc(obj): - savedrepo, savedwc = obj - #print >>sys.stderr, ("restoring", savedrepo) - repo = savedrepo.new(basename=savedrepo.basename[:-2]) - assert repo.check() - wc = savedwc.new(basename=savedwc.basename[:-2]) - assert wc.check() - wc.localpath.remove() - repo.remove() - savedrepo.move(repo) - savedwc.localpath.move(wc.localpath) - py.path.svnurl._lsnorevcache.clear() - py.path.svnurl._lsrevcache.clear() diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/repotest.dump b/tests/wpt/tests/tools/third_party/py/testing/path/repotest.dump deleted file mode 100644 index c7819cad7a5..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/repotest.dump +++ /dev/null @@ -1,228 +0,0 @@ -SVN-fs-dump-format-version: 2 - -UUID: 876a30f4-1eed-0310-aeb7-ae314d1e5934 - -Revision-number: 0 -Prop-content-length: 56 -Content-length: 56 - -K 8 -svn:date -V 27 -2005-01-07T23:55:31.755989Z -PROPS-END - -Revision-number: 1 -Prop-content-length: 118 -Content-length: 118 - -K 7 -svn:log -V 20 -testrepo setup rev 1 -K 10 -svn:author -V 3 -hpk -K 8 -svn:date -V 27 -2005-01-07T23:55:37.815386Z -PROPS-END - -Node-path: execfile -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 4 -Text-content-md5: d4b5bc61e16310f08c5d11866eba0a22 -Content-length: 14 - -PROPS-END -x=42 - -Node-path: otherdir -Node-kind: dir -Node-action: add -Prop-content-length: 10 -Content-length: 10 - -PROPS-END - - -Node-path: otherdir/__init__.py -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 0 -Text-content-md5: d41d8cd98f00b204e9800998ecf8427e -Content-length: 10 - -PROPS-END - - -Node-path: otherdir/a.py -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 30 -Text-content-md5: 247c7daeb2ee5dcab0aba7bd12bad665 -Content-length: 40 - -PROPS-END -from b import stuff as result - - -Node-path: otherdir/b.py -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 15 -Text-content-md5: c1b13503469a7711306d03a4b0721bc6 -Content-length: 25 - -PROPS-END -stuff="got it" - - -Node-path: otherdir/c.py -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 75 -Text-content-md5: 250cdb6b5df68536152c681f48297569 -Content-length: 85 - -PROPS-END -import py; py.magic.autopath() -import otherdir.a -value = otherdir.a.result - - -Node-path: otherdir/d.py -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 72 -Text-content-md5: 940c9c621e7b198e081459642c37f5a7 -Content-length: 82 - -PROPS-END -import py; py.magic.autopath() -from otherdir import a -value2 = a.result - - -Node-path: sampledir -Node-kind: dir -Node-action: add -Prop-content-length: 10 -Content-length: 10 - -PROPS-END - - -Node-path: sampledir/otherfile -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 0 -Text-content-md5: d41d8cd98f00b204e9800998ecf8427e -Content-length: 10 - -PROPS-END - - -Node-path: samplefile -Node-kind: file -Node-action: add -Prop-content-length: 40 -Text-content-length: 11 -Text-content-md5: 9225ac28b32156979ab6482b8bb5fb8c -Content-length: 51 - -K 13 -svn:eol-style -V 6 -native -PROPS-END -samplefile - - -Node-path: samplepickle -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 56 -Text-content-md5: 719d85c1329a33134bb98f56b756c545 -Content-length: 66 - -PROPS-END -(dp1 -S'answer' -p2 -I42 -sI1 -I2 -sS'hello' -p3 -S'world' -p4 -s. - -Revision-number: 2 -Prop-content-length: 108 -Content-length: 108 - -K 7 -svn:log -V 10 -second rev -K 10 -svn:author -V 3 -hpk -K 8 -svn:date -V 27 -2005-01-07T23:55:39.223202Z -PROPS-END - -Node-path: anotherfile -Node-kind: file -Node-action: add -Prop-content-length: 10 -Text-content-length: 5 -Text-content-md5: 5d41402abc4b2a76b9719d911017c592 -Content-length: 15 - -PROPS-END -hello - -Revision-number: 3 -Prop-content-length: 106 -Content-length: 106 - -K 7 -svn:log -V 9 -third rev -K 10 -svn:author -V 3 -hpk -K 8 -svn:date -V 27 -2005-01-07T23:55:41.556642Z -PROPS-END - -Node-path: anotherfile -Node-kind: file -Node-action: change -Text-content-length: 5 -Text-content-md5: 7d793037a0760186574b0282f2f435e7 -Content-length: 5 - -world - diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/svntestbase.py b/tests/wpt/tests/tools/third_party/py/testing/path/svntestbase.py deleted file mode 100644 index 8d94a9ca649..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/svntestbase.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -import py -from py._path import svnwc as svncommon -from common import CommonFSTests - -class CommonSvnTests(CommonFSTests): - - def test_propget(self, path1): - url = path1.join("samplefile") - value = url.propget('svn:eol-style') - assert value == 'native' - - def test_proplist(self, path1): - url = path1.join("samplefile") - res = url.proplist() - assert res['svn:eol-style'] == 'native' - - def test_info(self, path1): - url = path1.join("samplefile") - res = url.info() - assert res.size > len("samplefile") and res.created_rev >= 0 - - def test_log_simple(self, path1): - url = path1.join("samplefile") - logentries = url.log() - for logentry in logentries: - assert logentry.rev == 1 - assert hasattr(logentry, 'author') - assert hasattr(logentry, 'date') - -#cache.repositories.put(svnrepourl, 1200, 0) diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/test_cacheutil.py b/tests/wpt/tests/tools/third_party/py/testing/path/test_cacheutil.py deleted file mode 100644 index c9fc07463a7..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/test_cacheutil.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -from py._path import cacheutil - -import time - -class BasicCacheAPITest: - cache = None - def test_getorbuild(self): - val = self.cache.getorbuild(-42, lambda: 42) - assert val == 42 - val = self.cache.getorbuild(-42, lambda: 23) - assert val == 42 - - def test_cache_get_key_error(self): - pytest.raises(KeyError, "self.cache._getentry(-23)") - - def test_delentry_non_raising(self): - self.cache.getorbuild(100, lambda: 100) - self.cache.delentry(100) - pytest.raises(KeyError, "self.cache._getentry(100)") - - def test_delentry_raising(self): - self.cache.getorbuild(100, lambda: 100) - self.cache.delentry(100) - pytest.raises(KeyError, self.cache.delentry, 100, raising=True) - - def test_clear(self): - self.cache.clear() - - -class TestBuildcostAccess(BasicCacheAPITest): - cache = cacheutil.BuildcostAccessCache(maxentries=128) - - def test_cache_works_somewhat_simple(self, monkeypatch): - cache = cacheutil.BuildcostAccessCache() - # the default gettime - # BuildcostAccessCache.build can - # result into time()-time() == 0 which makes the below - # test fail randomly. Let's rather use incrementing - # numbers instead. - l = [0] - - def counter(): - l[0] = l[0] + 1 - return l[0] - monkeypatch.setattr(cacheutil, 'gettime', counter) - for x in range(cache.maxentries): - y = cache.getorbuild(x, lambda: x) - assert x == y - for x in range(cache.maxentries): - assert cache.getorbuild(x, None) == x - halfentries = int(cache.maxentries / 2) - for x in range(halfentries): - assert cache.getorbuild(x, None) == x - assert cache.getorbuild(x, None) == x - # evict one entry - val = cache.getorbuild(-1, lambda: 42) - assert val == 42 - # check that recently used ones are still there - # and are not build again - for x in range(halfentries): - assert cache.getorbuild(x, None) == x - assert cache.getorbuild(-1, None) == 42 - - -class TestAging(BasicCacheAPITest): - maxsecs = 0.10 - cache = cacheutil.AgingCache(maxentries=128, maxseconds=maxsecs) - - def test_cache_eviction(self): - self.cache.getorbuild(17, lambda: 17) - endtime = time.time() + self.maxsecs * 10 - while time.time() < endtime: - try: - self.cache._getentry(17) - except KeyError: - break - time.sleep(self.maxsecs*0.3) - else: - pytest.fail("waiting for cache eviction failed") - - -def test_prune_lowestweight(): - maxsecs = 0.05 - cache = cacheutil.AgingCache(maxentries=10, maxseconds=maxsecs) - for x in range(cache.maxentries): - cache.getorbuild(x, lambda: x) - time.sleep(maxsecs*1.1) - cache.getorbuild(cache.maxentries+1, lambda: 42) diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/test_local.py b/tests/wpt/tests/tools/third_party/py/testing/path/test_local.py deleted file mode 100644 index 1b9a7923f65..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/test_local.py +++ /dev/null @@ -1,1078 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import with_statement -import time -import py -import pytest -import os -import sys -import multiprocessing -from py.path import local -import common - -failsonjython = py.test.mark.xfail("sys.platform.startswith('java')") -failsonjywin32 = py.test.mark.xfail( - "sys.platform.startswith('java') " - "and getattr(os, '_name', None) == 'nt'") -win32only = py.test.mark.skipif( - "not (sys.platform == 'win32' or getattr(os, '_name', None) == 'nt')") -skiponwin32 = py.test.mark.skipif( - "sys.platform == 'win32' or getattr(os, '_name', None) == 'nt'") - -ATIME_RESOLUTION = 0.01 - - -@pytest.yield_fixture(scope="session") -def path1(tmpdir_factory): - path = tmpdir_factory.mktemp('path') - common.setuptestfs(path) - yield path - assert path.join("samplefile").check() - - -@pytest.fixture -def fake_fspath_obj(request): - class FakeFSPathClass(object): - def __init__(self, path): - self._path = path - - def __fspath__(self): - return self._path - - return FakeFSPathClass(os.path.join("this", "is", "a", "fake", "path")) - - -def batch_make_numbered_dirs(rootdir, repeats): - try: - for i in range(repeats): - dir_ = py.path.local.make_numbered_dir(prefix='repro-', rootdir=rootdir) - file_ = dir_.join('foo') - file_.write('%s' % i) - actual = int(file_.read()) - assert actual == i, 'int(file_.read()) is %s instead of %s' % (actual, i) - dir_.join('.lock').remove(ignore_errors=True) - return True - except KeyboardInterrupt: - # makes sure that interrupting test session won't hang it - os.exit(2) - - -class TestLocalPath(common.CommonFSTests): - def test_join_normpath(self, tmpdir): - assert tmpdir.join(".") == tmpdir - p = tmpdir.join("../%s" % tmpdir.basename) - assert p == tmpdir - p = tmpdir.join("..//%s/" % tmpdir.basename) - assert p == tmpdir - - @skiponwin32 - def test_dirpath_abs_no_abs(self, tmpdir): - p = tmpdir.join('foo') - assert p.dirpath('/bar') == tmpdir.join('bar') - assert tmpdir.dirpath('/bar', abs=True) == local('/bar') - - def test_gethash(self, tmpdir): - md5 = py.builtin._tryimport('md5', 'hashlib').md5 - lib = py.builtin._tryimport('sha', 'hashlib') - sha = getattr(lib, 'sha1', getattr(lib, 'sha', None)) - fn = tmpdir.join("testhashfile") - data = 'hello'.encode('ascii') - fn.write(data, mode="wb") - assert fn.computehash("md5") == md5(data).hexdigest() - assert fn.computehash("sha1") == sha(data).hexdigest() - py.test.raises(ValueError, fn.computehash, "asdasd") - - def test_remove_removes_readonly_file(self, tmpdir): - readonly_file = tmpdir.join('readonly').ensure() - readonly_file.chmod(0) - readonly_file.remove() - assert not readonly_file.check(exists=1) - - def test_remove_removes_readonly_dir(self, tmpdir): - readonly_dir = tmpdir.join('readonlydir').ensure(dir=1) - readonly_dir.chmod(int("500", 8)) - readonly_dir.remove() - assert not readonly_dir.check(exists=1) - - def test_remove_removes_dir_and_readonly_file(self, tmpdir): - readonly_dir = tmpdir.join('readonlydir').ensure(dir=1) - readonly_file = readonly_dir.join('readonlyfile').ensure() - readonly_file.chmod(0) - readonly_dir.remove() - assert not readonly_dir.check(exists=1) - - def test_remove_routes_ignore_errors(self, tmpdir, monkeypatch): - l = [] - monkeypatch.setattr( - 'shutil.rmtree', - lambda *args, **kwargs: l.append(kwargs)) - tmpdir.remove() - assert not l[0]['ignore_errors'] - for val in (True, False): - l[:] = [] - tmpdir.remove(ignore_errors=val) - assert l[0]['ignore_errors'] == val - - def test_initialize_curdir(self): - assert str(local()) == os.getcwd() - - @skiponwin32 - def test_chdir_gone(self, path1): - p = path1.ensure("dir_to_be_removed", dir=1) - p.chdir() - p.remove() - pytest.raises(py.error.ENOENT, py.path.local) - assert path1.chdir() is None - assert os.getcwd() == str(path1) - - with pytest.raises(py.error.ENOENT): - with p.as_cwd(): - raise NotImplementedError - - @skiponwin32 - def test_chdir_gone_in_as_cwd(self, path1): - p = path1.ensure("dir_to_be_removed", dir=1) - p.chdir() - p.remove() - - with path1.as_cwd() as old: - assert old is None - - def test_as_cwd(self, path1): - dir = path1.ensure("subdir", dir=1) - old = py.path.local() - with dir.as_cwd() as x: - assert x == old - assert py.path.local() == dir - assert os.getcwd() == str(old) - - def test_as_cwd_exception(self, path1): - old = py.path.local() - dir = path1.ensure("subdir", dir=1) - with pytest.raises(ValueError): - with dir.as_cwd(): - raise ValueError() - assert old == py.path.local() - - def test_initialize_reldir(self, path1): - with path1.as_cwd(): - p = local('samplefile') - assert p.check() - - def test_tilde_expansion(self, monkeypatch, tmpdir): - monkeypatch.setenv("HOME", str(tmpdir)) - p = py.path.local("~", expanduser=True) - assert p == os.path.expanduser("~") - - @pytest.mark.skipif( - not sys.platform.startswith("win32"), reason="case insensitive only on windows" - ) - def test_eq_hash_are_case_insensitive_on_windows(self): - a = py.path.local("/some/path") - b = py.path.local("/some/PATH") - assert a == b - assert hash(a) == hash(b) - assert a in {b} - assert a in {b: 'b'} - - def test_eq_with_strings(self, path1): - path1 = path1.join('sampledir') - path2 = str(path1) - assert path1 == path2 - assert path2 == path1 - path3 = path1.join('samplefile') - assert path3 != path2 - assert path2 != path3 - - def test_eq_with_none(self, path1): - assert path1 != None # noqa: E711 - - @pytest.mark.skipif( - sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" - ) - @pytest.mark.skipif( - sys.version_info < (3, 0) or sys.version_info >= (3, 5), - reason="only with Python 3 before 3.5" - ) - def test_eq_with_none_and_custom_fspath(self, monkeypatch, path1): - import os - import shutil - import tempfile - - d = tempfile.mkdtemp() - monkeypatch.chdir(d) - shutil.rmtree(d) - - monkeypatch.delitem(sys.modules, 'pathlib', raising=False) - monkeypatch.setattr(sys, 'path', [''] + sys.path) - - with pytest.raises(FileNotFoundError): - import pathlib # noqa: F401 - - assert path1 != None # noqa: E711 - - def test_eq_non_ascii_unicode(self, path1): - path2 = path1.join(u'temp') - path3 = path1.join(u'ação') - path4 = path1.join(u'ディレクトリ') - - assert path2 != path3 - assert path2 != path4 - assert path4 != path3 - - def test_gt_with_strings(self, path1): - path2 = path1.join('sampledir') - path3 = str(path1.join("ttt")) - assert path3 > path2 - assert path2 < path3 - assert path2 < "ttt" - assert "ttt" > path2 - path4 = path1.join("aaa") - l = [path2, path4, path3] - assert sorted(l) == [path4, path2, path3] - - def test_open_and_ensure(self, path1): - p = path1.join("sub1", "sub2", "file") - with p.open("w", ensure=1) as f: - f.write("hello") - assert p.read() == "hello" - - def test_write_and_ensure(self, path1): - p = path1.join("sub1", "sub2", "file") - p.write("hello", ensure=1) - assert p.read() == "hello" - - @py.test.mark.parametrize('bin', (False, True)) - def test_dump(self, tmpdir, bin): - path = tmpdir.join("dumpfile%s" % int(bin)) - try: - d = {'answer': 42} - path.dump(d, bin=bin) - f = path.open('rb+') - import pickle - dnew = pickle.load(f) - assert d == dnew - finally: - f.close() - - @failsonjywin32 - def test_setmtime(self): - import tempfile - import time - try: - fd, name = tempfile.mkstemp() - os.close(fd) - except AttributeError: - name = tempfile.mktemp() - open(name, 'w').close() - try: - mtime = int(time.time())-100 - path = local(name) - assert path.mtime() != mtime - path.setmtime(mtime) - assert path.mtime() == mtime - path.setmtime() - assert path.mtime() != mtime - finally: - os.remove(name) - - def test_normpath(self, path1): - new1 = path1.join("/otherdir") - new2 = path1.join("otherdir") - assert str(new1) == str(new2) - - def test_mkdtemp_creation(self): - d = local.mkdtemp() - try: - assert d.check(dir=1) - finally: - d.remove(rec=1) - - def test_tmproot(self): - d = local.mkdtemp() - tmproot = local.get_temproot() - try: - assert d.check(dir=1) - assert d.dirpath() == tmproot - finally: - d.remove(rec=1) - - def test_chdir(self, tmpdir): - old = local() - try: - res = tmpdir.chdir() - assert str(res) == str(old) - assert os.getcwd() == str(tmpdir) - finally: - old.chdir() - - def test_ensure_filepath_withdir(self, tmpdir): - newfile = tmpdir.join('test1', 'test') - newfile.ensure() - assert newfile.check(file=1) - newfile.write("42") - newfile.ensure() - s = newfile.read() - assert s == "42" - - def test_ensure_filepath_withoutdir(self, tmpdir): - newfile = tmpdir.join('test1file') - t = newfile.ensure() - assert t == newfile - assert newfile.check(file=1) - - def test_ensure_dirpath(self, tmpdir): - newfile = tmpdir.join('test1', 'testfile') - t = newfile.ensure(dir=1) - assert t == newfile - assert newfile.check(dir=1) - - def test_ensure_non_ascii_unicode(self, tmpdir): - newfile = tmpdir.join(u'ação',u'ディレクトリ') - t = newfile.ensure(dir=1) - assert t == newfile - assert newfile.check(dir=1) - - def test_init_from_path(self, tmpdir): - l = local() - l2 = local(l) - assert l2 == l - - wc = py.path.svnwc('.') - l3 = local(wc) - assert l3 is not wc - assert l3.strpath == wc.strpath - assert not hasattr(l3, 'commit') - - @py.test.mark.xfail(run=False, reason="unreliable est for long filenames") - def test_long_filenames(self, tmpdir): - if sys.platform == "win32": - py.test.skip("win32: work around needed for path length limit") - # see http://codespeak.net/pipermail/py-dev/2008q2/000922.html - - # testing paths > 260 chars (which is Windows' limitation, but - # depending on how the paths are used), but > 4096 (which is the - # Linux' limitation) - the behaviour of paths with names > 4096 chars - # is undetermined - newfilename = '/test' * 60 - l = tmpdir.join(newfilename) - l.ensure(file=True) - l.write('foo') - l2 = tmpdir.join(newfilename) - assert l2.read() == 'foo' - - def test_visit_depth_first(self, tmpdir): - tmpdir.ensure("a", "1") - tmpdir.ensure("b", "2") - p3 = tmpdir.ensure("breadth") - l = list(tmpdir.visit(lambda x: x.check(file=1))) - assert len(l) == 3 - # check that breadth comes last - assert l[2] == p3 - - def test_visit_rec_fnmatch(self, tmpdir): - p1 = tmpdir.ensure("a", "123") - tmpdir.ensure(".b", "345") - l = list(tmpdir.visit("???", rec="[!.]*")) - assert len(l) == 1 - # check that breadth comes last - assert l[0] == p1 - - def test_fnmatch_file_abspath(self, tmpdir): - b = tmpdir.join("a", "b") - assert b.fnmatch(os.sep.join("ab")) - pattern = os.sep.join([str(tmpdir), "*", "b"]) - assert b.fnmatch(pattern) - - def test_sysfind(self): - name = sys.platform == "win32" and "cmd" or "test" - x = py.path.local.sysfind(name) - assert x.check(file=1) - assert py.path.local.sysfind('jaksdkasldqwe') is None - assert py.path.local.sysfind(name, paths=[]) is None - x2 = py.path.local.sysfind(name, paths=[x.dirpath()]) - assert x2 == x - - def test_fspath_protocol_other_class(self, fake_fspath_obj): - # py.path is always absolute - py_path = py.path.local(fake_fspath_obj) - str_path = fake_fspath_obj.__fspath__() - assert py_path.check(endswith=str_path) - assert py_path.join(fake_fspath_obj).strpath == os.path.join( - py_path.strpath, str_path) - - def test_make_numbered_dir_multiprocess_safe(self, tmpdir): - # https://github.com/pytest-dev/py/issues/30 - pool = multiprocessing.Pool() - results = [pool.apply_async(batch_make_numbered_dirs, [tmpdir, 100]) for _ in range(20)] - for r in results: - assert r.get() - - -class TestExecutionOnWindows: - pytestmark = win32only - - def test_sysfind_bat_exe_before(self, tmpdir, monkeypatch): - monkeypatch.setenv("PATH", str(tmpdir), prepend=os.pathsep) - tmpdir.ensure("hello") - h = tmpdir.ensure("hello.bat") - x = py.path.local.sysfind("hello") - assert x == h - - -class TestExecution: - pytestmark = skiponwin32 - - def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir): - noperm = tmpdir.ensure('noperm', dir=True) - monkeypatch.setenv("PATH", noperm, prepend=":") - noperm.chmod(0) - assert py.path.local.sysfind('jaksdkasldqwe') is None - - def test_sysfind_absolute(self): - x = py.path.local.sysfind('test') - assert x.check(file=1) - y = py.path.local.sysfind(str(x)) - assert y.check(file=1) - assert y == x - - def test_sysfind_multiple(self, tmpdir, monkeypatch): - monkeypatch.setenv('PATH', "%s:%s" % ( - tmpdir.ensure('a'), - tmpdir.join('b')), - prepend=":") - tmpdir.ensure('b', 'a') - x = py.path.local.sysfind( - 'a', checker=lambda x: x.dirpath().basename == 'b') - assert x.basename == 'a' - assert x.dirpath().basename == 'b' - assert py.path.local.sysfind('a', checker=lambda x: None) is None - - def test_sysexec(self): - x = py.path.local.sysfind('ls') - out = x.sysexec('-a') - for x in py.path.local().listdir(): - assert out.find(x.basename) != -1 - - def test_sysexec_failing(self): - x = py.path.local.sysfind('false') - with pytest.raises(py.process.cmdexec.Error): - x.sysexec('aksjdkasjd') - - def test_make_numbered_dir(self, tmpdir): - tmpdir.ensure('base.not_an_int', dir=1) - for i in range(10): - numdir = local.make_numbered_dir(prefix='base.', rootdir=tmpdir, - keep=2, lock_timeout=0) - assert numdir.check() - assert numdir.basename == 'base.%d' % i - if i >= 1: - assert numdir.new(ext=str(i-1)).check() - if i >= 2: - assert numdir.new(ext=str(i-2)).check() - if i >= 3: - assert not numdir.new(ext=str(i-3)).check() - - def test_make_numbered_dir_case(self, tmpdir): - """make_numbered_dir does not make assumptions on the underlying - filesystem based on the platform and will assume it _could_ be case - insensitive. - - See issues: - - https://github.com/pytest-dev/pytest/issues/708 - - https://github.com/pytest-dev/pytest/issues/3451 - """ - d1 = local.make_numbered_dir( - prefix='CAse.', rootdir=tmpdir, keep=2, lock_timeout=0, - ) - d2 = local.make_numbered_dir( - prefix='caSE.', rootdir=tmpdir, keep=2, lock_timeout=0, - ) - assert str(d1).lower() != str(d2).lower() - assert str(d2).endswith('.1') - - def test_make_numbered_dir_NotImplemented_Error(self, tmpdir, monkeypatch): - def notimpl(x, y): - raise NotImplementedError(42) - monkeypatch.setattr(os, 'symlink', notimpl) - x = tmpdir.make_numbered_dir(rootdir=tmpdir, lock_timeout=0) - assert x.relto(tmpdir) - assert x.check() - - def test_locked_make_numbered_dir(self, tmpdir): - for i in range(10): - numdir = local.make_numbered_dir(prefix='base2.', rootdir=tmpdir, - keep=2) - assert numdir.check() - assert numdir.basename == 'base2.%d' % i - for j in range(i): - assert numdir.new(ext=str(j)).check() - - def test_error_preservation(self, path1): - py.test.raises(EnvironmentError, path1.join('qwoeqiwe').mtime) - py.test.raises(EnvironmentError, path1.join('qwoeqiwe').read) - - # def test_parentdirmatch(self): - # local.parentdirmatch('std', startmodule=__name__) - # - - -class TestImport: - def test_pyimport(self, path1): - obj = path1.join('execfile.py').pyimport() - assert obj.x == 42 - assert obj.__name__ == 'execfile' - - def test_pyimport_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): - p = tmpdir.ensure("a", "test_x123.py") - p.pyimport() - tmpdir.join("a").move(tmpdir.join("b")) - with pytest.raises(tmpdir.ImportMismatchError): - tmpdir.join("b", "test_x123.py").pyimport() - - # Errors can be ignored. - monkeypatch.setenv('PY_IGNORE_IMPORTMISMATCH', '1') - tmpdir.join("b", "test_x123.py").pyimport() - - # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. - monkeypatch.setenv('PY_IGNORE_IMPORTMISMATCH', '0') - with pytest.raises(tmpdir.ImportMismatchError): - tmpdir.join("b", "test_x123.py").pyimport() - - def test_pyimport_messy_name(self, tmpdir): - # http://bitbucket.org/hpk42/py-trunk/issue/129 - path = tmpdir.ensure('foo__init__.py') - path.pyimport() - - def test_pyimport_dir(self, tmpdir): - p = tmpdir.join("hello_123") - p_init = p.ensure("__init__.py") - m = p.pyimport() - assert m.__name__ == "hello_123" - m = p_init.pyimport() - assert m.__name__ == "hello_123" - - def test_pyimport_execfile_different_name(self, path1): - obj = path1.join('execfile.py').pyimport(modname="0x.y.z") - assert obj.x == 42 - assert obj.__name__ == '0x.y.z' - - def test_pyimport_a(self, path1): - otherdir = path1.join('otherdir') - mod = otherdir.join('a.py').pyimport() - assert mod.result == "got it" - assert mod.__name__ == 'otherdir.a' - - def test_pyimport_b(self, path1): - otherdir = path1.join('otherdir') - mod = otherdir.join('b.py').pyimport() - assert mod.stuff == "got it" - assert mod.__name__ == 'otherdir.b' - - def test_pyimport_c(self, path1): - otherdir = path1.join('otherdir') - mod = otherdir.join('c.py').pyimport() - assert mod.value == "got it" - - def test_pyimport_d(self, path1): - otherdir = path1.join('otherdir') - mod = otherdir.join('d.py').pyimport() - assert mod.value2 == "got it" - - def test_pyimport_and_import(self, tmpdir): - tmpdir.ensure('xxxpackage', '__init__.py') - mod1path = tmpdir.ensure('xxxpackage', 'module1.py') - mod1 = mod1path.pyimport() - assert mod1.__name__ == 'xxxpackage.module1' - from xxxpackage import module1 - assert module1 is mod1 - - def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir): - name = 'pointsback123' - ModuleType = type(os) - p = tmpdir.ensure(name + '.py') - for ending in ('.pyc', '$py.class', '.pyo'): - mod = ModuleType(name) - pseudopath = tmpdir.ensure(name+ending) - mod.__file__ = str(pseudopath) - monkeypatch.setitem(sys.modules, name, mod) - newmod = p.pyimport() - assert mod == newmod - monkeypatch.undo() - mod = ModuleType(name) - pseudopath = tmpdir.ensure(name+"123.py") - mod.__file__ = str(pseudopath) - monkeypatch.setitem(sys.modules, name, mod) - excinfo = py.test.raises(pseudopath.ImportMismatchError, p.pyimport) - modname, modfile, orig = excinfo.value.args - assert modname == name - assert modfile == pseudopath - assert orig == p - assert issubclass(pseudopath.ImportMismatchError, ImportError) - - def test_issue131_pyimport_on__init__(self, tmpdir): - # __init__.py files may be namespace packages, and thus the - # __file__ of an imported module may not be ourselves - # see issue - p1 = tmpdir.ensure("proja", "__init__.py") - p2 = tmpdir.ensure("sub", "proja", "__init__.py") - m1 = p1.pyimport() - m2 = p2.pyimport() - assert m1 == m2 - - def test_ensuresyspath_append(self, tmpdir): - root1 = tmpdir.mkdir("root1") - file1 = root1.ensure("x123.py") - assert str(root1) not in sys.path - file1.pyimport(ensuresyspath="append") - assert str(root1) == sys.path[-1] - assert str(root1) not in sys.path[:-1] - - -class TestImportlibImport: - pytestmark = py.test.mark.skipif("sys.version_info < (3, 5)") - - OPTS = {'ensuresyspath': 'importlib'} - - def test_pyimport(self, path1): - obj = path1.join('execfile.py').pyimport(**self.OPTS) - assert obj.x == 42 - assert obj.__name__ == 'execfile' - - def test_pyimport_dir_fails(self, tmpdir): - p = tmpdir.join("hello_123") - p.ensure("__init__.py") - with pytest.raises(ImportError): - p.pyimport(**self.OPTS) - - def test_pyimport_execfile_different_name(self, path1): - obj = path1.join('execfile.py').pyimport(modname="0x.y.z", **self.OPTS) - assert obj.x == 42 - assert obj.__name__ == '0x.y.z' - - def test_pyimport_relative_import_fails(self, path1): - otherdir = path1.join('otherdir') - with pytest.raises(ImportError): - otherdir.join('a.py').pyimport(**self.OPTS) - - def test_pyimport_doesnt_use_sys_modules(self, tmpdir): - p = tmpdir.ensure('file738jsk.py') - mod = p.pyimport(**self.OPTS) - assert mod.__name__ == 'file738jsk' - assert 'file738jsk' not in sys.modules - - -def test_pypkgdir(tmpdir): - pkg = tmpdir.ensure('pkg1', dir=1) - pkg.ensure("__init__.py") - pkg.ensure("subdir/__init__.py") - assert pkg.pypkgpath() == pkg - assert pkg.join('subdir', '__init__.py').pypkgpath() == pkg - - -def test_pypkgdir_unimportable(tmpdir): - pkg = tmpdir.ensure('pkg1-1', dir=1) # unimportable - pkg.ensure("__init__.py") - subdir = pkg.ensure("subdir/__init__.py").dirpath() - assert subdir.pypkgpath() == subdir - assert subdir.ensure("xyz.py").pypkgpath() == subdir - assert not pkg.pypkgpath() - - -def test_isimportable(): - from py._path.local import isimportable - assert not isimportable("") - assert isimportable("x") - assert isimportable("x1") - assert isimportable("x_1") - assert isimportable("_") - assert isimportable("_1") - assert not isimportable("x-1") - assert not isimportable("x:1") - - -def test_homedir_from_HOME(monkeypatch): - path = os.getcwd() - monkeypatch.setenv("HOME", path) - assert py.path.local._gethomedir() == py.path.local(path) - - -def test_homedir_not_exists(monkeypatch): - monkeypatch.delenv("HOME", raising=False) - monkeypatch.delenv("HOMEDRIVE", raising=False) - homedir = py.path.local._gethomedir() - assert homedir is None - - -def test_samefile(tmpdir): - assert tmpdir.samefile(tmpdir) - p = tmpdir.ensure("hello") - assert p.samefile(p) - with p.dirpath().as_cwd(): - assert p.samefile(p.basename) - if sys.platform == "win32": - p1 = p.__class__(str(p).lower()) - p2 = p.__class__(str(p).upper()) - assert p1.samefile(p2) - -@pytest.mark.skipif(not hasattr(os, "symlink"), reason="os.symlink not available") -def test_samefile_symlink(tmpdir): - p1 = tmpdir.ensure("foo.txt") - p2 = tmpdir.join("linked.txt") - try: - os.symlink(str(p1), str(p2)) - except (OSError, NotImplementedError) as e: - # on Windows this might fail if the user doesn't have special symlink permissions - # pypy3 on Windows doesn't implement os.symlink and raises NotImplementedError - pytest.skip(str(e.args[0])) - - assert p1.samefile(p2) - -def test_listdir_single_arg(tmpdir): - tmpdir.ensure("hello") - assert tmpdir.listdir("hello")[0].basename == "hello" - - -def test_mkdtemp_rootdir(tmpdir): - dtmp = local.mkdtemp(rootdir=tmpdir) - assert tmpdir.listdir() == [dtmp] - - -class TestWINLocalPath: - pytestmark = win32only - - def test_owner_group_not_implemented(self, path1): - py.test.raises(NotImplementedError, "path1.stat().owner") - py.test.raises(NotImplementedError, "path1.stat().group") - - def test_chmod_simple_int(self, path1): - py.builtin.print_("path1 is", path1) - mode = path1.stat().mode - # Ensure that we actually change the mode to something different. - path1.chmod(mode == 0 and 1 or 0) - try: - print(path1.stat().mode) - print(mode) - assert path1.stat().mode != mode - finally: - path1.chmod(mode) - assert path1.stat().mode == mode - - def test_path_comparison_lowercase_mixed(self, path1): - t1 = path1.join("a_path") - t2 = path1.join("A_path") - assert t1 == t1 - assert t1 == t2 - - def test_relto_with_mixed_case(self, path1): - t1 = path1.join("a_path", "fiLe") - t2 = path1.join("A_path") - assert t1.relto(t2) == "fiLe" - - def test_allow_unix_style_paths(self, path1): - t1 = path1.join('a_path') - assert t1 == str(path1) + '\\a_path' - t1 = path1.join('a_path/') - assert t1 == str(path1) + '\\a_path' - t1 = path1.join('dir/a_path') - assert t1 == str(path1) + '\\dir\\a_path' - - def test_sysfind_in_currentdir(self, path1): - cmd = py.path.local.sysfind('cmd') - root = cmd.new(dirname='', basename='') # c:\ in most installations - with root.as_cwd(): - x = py.path.local.sysfind(cmd.relto(root)) - assert x.check(file=1) - - def test_fnmatch_file_abspath_posix_pattern_on_win32(self, tmpdir): - # path-matching patterns might contain a posix path separator '/' - # Test that we can match that pattern on windows. - import posixpath - b = tmpdir.join("a", "b") - assert b.fnmatch(posixpath.sep.join("ab")) - pattern = posixpath.sep.join([str(tmpdir), "*", "b"]) - assert b.fnmatch(pattern) - - -class TestPOSIXLocalPath: - pytestmark = skiponwin32 - - def test_hardlink(self, tmpdir): - linkpath = tmpdir.join('test') - filepath = tmpdir.join('file') - filepath.write("Hello") - nlink = filepath.stat().nlink - linkpath.mklinkto(filepath) - assert filepath.stat().nlink == nlink + 1 - - def test_symlink_are_identical(self, tmpdir): - filepath = tmpdir.join('file') - filepath.write("Hello") - linkpath = tmpdir.join('test') - linkpath.mksymlinkto(filepath) - assert linkpath.readlink() == str(filepath) - - def test_symlink_isfile(self, tmpdir): - linkpath = tmpdir.join('test') - filepath = tmpdir.join('file') - filepath.write("") - linkpath.mksymlinkto(filepath) - assert linkpath.check(file=1) - assert not linkpath.check(link=0, file=1) - assert linkpath.islink() - - def test_symlink_relative(self, tmpdir): - linkpath = tmpdir.join('test') - filepath = tmpdir.join('file') - filepath.write("Hello") - linkpath.mksymlinkto(filepath, absolute=False) - assert linkpath.readlink() == "file" - assert filepath.read() == linkpath.read() - - def test_symlink_not_existing(self, tmpdir): - linkpath = tmpdir.join('testnotexisting') - assert not linkpath.check(link=1) - assert linkpath.check(link=0) - - def test_relto_with_root(self, path1, tmpdir): - y = path1.join('x').relto(py.path.local('/')) - assert y[0] == str(path1)[1] - - def test_visit_recursive_symlink(self, tmpdir): - linkpath = tmpdir.join('test') - linkpath.mksymlinkto(tmpdir) - visitor = tmpdir.visit(None, lambda x: x.check(link=0)) - assert list(visitor) == [linkpath] - - def test_symlink_isdir(self, tmpdir): - linkpath = tmpdir.join('test') - linkpath.mksymlinkto(tmpdir) - assert linkpath.check(dir=1) - assert not linkpath.check(link=0, dir=1) - - def test_symlink_remove(self, tmpdir): - linkpath = tmpdir.join('test') - linkpath.mksymlinkto(linkpath) # point to itself - assert linkpath.check(link=1) - linkpath.remove() - assert not linkpath.check() - - def test_realpath_file(self, tmpdir): - linkpath = tmpdir.join('test') - filepath = tmpdir.join('file') - filepath.write("") - linkpath.mksymlinkto(filepath) - realpath = linkpath.realpath() - assert realpath.basename == 'file' - - def test_owner(self, path1, tmpdir): - from pwd import getpwuid - from grp import getgrgid - stat = path1.stat() - assert stat.path == path1 - - uid = stat.uid - gid = stat.gid - owner = getpwuid(uid)[0] - group = getgrgid(gid)[0] - - assert uid == stat.uid - assert owner == stat.owner - assert gid == stat.gid - assert group == stat.group - - def test_stat_helpers(self, tmpdir, monkeypatch): - path1 = tmpdir.ensure("file") - stat1 = path1.stat() - stat2 = tmpdir.stat() - assert stat1.isfile() - assert stat2.isdir() - assert not stat1.islink() - assert not stat2.islink() - - def test_stat_non_raising(self, tmpdir): - path1 = tmpdir.join("file") - pytest.raises(py.error.ENOENT, lambda: path1.stat()) - res = path1.stat(raising=False) - assert res is None - - def test_atime(self, tmpdir): - import time - path = tmpdir.ensure('samplefile') - now = time.time() - atime1 = path.atime() - # we could wait here but timer resolution is very - # system dependent - path.read() - time.sleep(ATIME_RESOLUTION) - atime2 = path.atime() - time.sleep(ATIME_RESOLUTION) - duration = time.time() - now - assert (atime2-atime1) <= duration - - def test_commondir(self, path1): - # XXX This is here in local until we find a way to implement this - # using the subversion command line api. - p1 = path1.join('something') - p2 = path1.join('otherthing') - assert p1.common(p2) == path1 - assert p2.common(p1) == path1 - - def test_commondir_nocommon(self, path1): - # XXX This is here in local until we find a way to implement this - # using the subversion command line api. - p1 = path1.join('something') - p2 = py.path.local(path1.sep+'blabla') - assert p1.common(p2) == '/' - - def test_join_to_root(self, path1): - root = path1.parts()[0] - assert len(str(root)) == 1 - assert str(root.join('a')) == '/a' - - def test_join_root_to_root_with_no_abs(self, path1): - nroot = path1.join('/') - assert str(path1) == str(nroot) - assert path1 == nroot - - def test_chmod_simple_int(self, path1): - mode = path1.stat().mode - path1.chmod(int(mode/2)) - try: - assert path1.stat().mode != mode - finally: - path1.chmod(mode) - assert path1.stat().mode == mode - - def test_chmod_rec_int(self, path1): - # XXX fragile test - def recfilter(x): return x.check(dotfile=0, link=0) - oldmodes = {} - for x in path1.visit(rec=recfilter): - oldmodes[x] = x.stat().mode - path1.chmod(int("772", 8), rec=recfilter) - try: - for x in path1.visit(rec=recfilter): - assert x.stat().mode & int("777", 8) == int("772", 8) - finally: - for x, y in oldmodes.items(): - x.chmod(y) - - def test_copy_archiving(self, tmpdir): - unicode_fn = u"something-\342\200\223.txt" - f = tmpdir.ensure("a", unicode_fn) - a = f.dirpath() - oldmode = f.stat().mode - newmode = oldmode ^ 1 - f.chmod(newmode) - b = tmpdir.join("b") - a.copy(b, mode=True) - assert b.join(f.basename).stat().mode == newmode - - def test_copy_stat_file(self, tmpdir): - src = tmpdir.ensure('src') - dst = tmpdir.join('dst') - # a small delay before the copy - time.sleep(ATIME_RESOLUTION) - src.copy(dst, stat=True) - oldstat = src.stat() - newstat = dst.stat() - assert oldstat.mode == newstat.mode - assert (dst.atime() - src.atime()) < ATIME_RESOLUTION - assert (dst.mtime() - src.mtime()) < ATIME_RESOLUTION - - def test_copy_stat_dir(self, tmpdir): - test_files = ['a', 'b', 'c'] - src = tmpdir.join('src') - for f in test_files: - src.join(f).write(f, ensure=True) - dst = tmpdir.join('dst') - # a small delay before the copy - time.sleep(ATIME_RESOLUTION) - src.copy(dst, stat=True) - for f in test_files: - oldstat = src.join(f).stat() - newstat = dst.join(f).stat() - assert (newstat.atime - oldstat.atime) < ATIME_RESOLUTION - assert (newstat.mtime - oldstat.mtime) < ATIME_RESOLUTION - assert oldstat.mode == newstat.mode - - @failsonjython - def test_chown_identity(self, path1): - owner = path1.stat().owner - group = path1.stat().group - path1.chown(owner, group) - - @failsonjython - def test_chown_dangling_link(self, path1): - owner = path1.stat().owner - group = path1.stat().group - x = path1.join('hello') - x.mksymlinkto('qlwkejqwlek') - try: - path1.chown(owner, group, rec=1) - finally: - x.remove(rec=0) - - @failsonjython - def test_chown_identity_rec_mayfail(self, path1): - owner = path1.stat().owner - group = path1.stat().group - path1.chown(owner, group) - - -class TestUnicodePy2Py3: - def test_join_ensure(self, tmpdir, monkeypatch): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: - pytest.skip("cannot run test without locale") - x = py.path.local(tmpdir.strpath) - part = "hällo" - y = x.ensure(part) - assert x.join(part) == y - - def test_listdir(self, tmpdir): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: - pytest.skip("cannot run test without locale") - x = py.path.local(tmpdir.strpath) - part = "hällo" - y = x.ensure(part) - assert x.listdir(part)[0] == y - - @pytest.mark.xfail( - reason="changing read/write might break existing usages") - def test_read_write(self, tmpdir): - x = tmpdir.join("hello") - part = py.builtin._totext("hällo", "utf8") - x.write(part) - assert x.read() == part - x.write(part.encode(sys.getdefaultencoding())) - assert x.read() == part.encode(sys.getdefaultencoding()) - - -class TestBinaryAndTextMethods: - def test_read_binwrite(self, tmpdir): - x = tmpdir.join("hello") - part = py.builtin._totext("hällo", "utf8") - part_utf8 = part.encode("utf8") - x.write_binary(part_utf8) - assert x.read_binary() == part_utf8 - s = x.read_text(encoding="utf8") - assert s == part - assert py.builtin._istext(s) - - def test_read_textwrite(self, tmpdir): - x = tmpdir.join("hello") - part = py.builtin._totext("hällo", "utf8") - part_utf8 = part.encode("utf8") - x.write_text(part, encoding="utf8") - assert x.read_binary() == part_utf8 - assert x.read_text(encoding="utf8") == part - - def test_default_encoding(self, tmpdir): - x = tmpdir.join("hello") - # Can't use UTF8 as the default encoding (ASCII) doesn't support it - part = py.builtin._totext("hello", "ascii") - x.write_text(part, "ascii") - s = x.read_text("ascii") - assert s == part - assert type(s) == type(part) diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnauth.py b/tests/wpt/tests/tools/third_party/py/testing/path/test_svnauth.py deleted file mode 100644 index 654f033224f..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnauth.py +++ /dev/null @@ -1,460 +0,0 @@ -import py -from py.path import SvnAuth -import time -import sys - -svnbin = py.path.local.sysfind('svn') - - -def make_repo_auth(repo, userdata): - """ write config to repo - - user information in userdata is used for auth - userdata has user names as keys, and a tuple (password, readwrite) as - values, where 'readwrite' is either 'r' or 'rw' - """ - confdir = py.path.local(repo).join('conf') - confdir.join('svnserve.conf').write('''\ -[general] -anon-access = none -password-db = passwd -authz-db = authz -realm = TestRepo -''') - authzdata = '[/]\n' - passwddata = '[users]\n' - for user in userdata: - authzdata += '%s = %s\n' % (user, userdata[user][1]) - passwddata += '%s = %s\n' % (user, userdata[user][0]) - confdir.join('authz').write(authzdata) - confdir.join('passwd').write(passwddata) - -def serve_bg(repopath): - pidfile = py.path.local(repopath).join('pid') - port = 10000 - e = None - while port < 10010: - cmd = 'svnserve -d -T --listen-port=%d --pid-file=%s -r %s' % ( - port, pidfile, repopath) - print(cmd) - try: - py.process.cmdexec(cmd) - except py.process.cmdexec.Error: - e = sys.exc_info()[1] - else: - # XXX we assume here that the pid file gets written somewhere, I - # guess this should be relatively safe... (I hope, at least?) - counter = pid = 0 - while counter < 10: - counter += 1 - try: - pid = pidfile.read() - except py.error.ENOENT: - pass - if pid: - break - time.sleep(0.2) - return port, int(pid) - port += 1 - raise IOError('could not start svnserve: %s' % (e,)) - -class TestSvnAuth(object): - def test_basic(self): - auth = SvnAuth('foo', 'bar') - assert auth.username == 'foo' - assert auth.password == 'bar' - assert str(auth) - - def test_makecmdoptions_uname_pw_makestr(self): - auth = SvnAuth('foo', 'bar') - assert auth.makecmdoptions() == '--username="foo" --password="bar"' - - def test_makecmdoptions_quote_escape(self): - auth = SvnAuth('fo"o', '"ba\'r"') - assert auth.makecmdoptions() == '--username="fo\\"o" --password="\\"ba\'r\\""' - - def test_makecmdoptions_no_cache_auth(self): - auth = SvnAuth('foo', 'bar', cache_auth=False) - assert auth.makecmdoptions() == ('--username="foo" --password="bar" ' - '--no-auth-cache') - - def test_makecmdoptions_no_interactive(self): - auth = SvnAuth('foo', 'bar', interactive=False) - assert auth.makecmdoptions() == ('--username="foo" --password="bar" ' - '--non-interactive') - - def test_makecmdoptions_no_interactive_no_cache_auth(self): - auth = SvnAuth('foo', 'bar', cache_auth=False, - interactive=False) - assert auth.makecmdoptions() == ('--username="foo" --password="bar" ' - '--no-auth-cache --non-interactive') - -class svnwc_no_svn(py.path.svnwc): - def __new__(cls, *args, **kwargs): - self = super(svnwc_no_svn, cls).__new__(cls, *args, **kwargs) - self.commands = [] - return self - - def _svn(self, *args): - self.commands.append(args) - -class TestSvnWCAuth(object): - def setup_method(self, meth): - if not svnbin: - py.test.skip("svn binary required") - self.auth = SvnAuth('user', 'pass', cache_auth=False) - - def test_checkout(self): - wc = svnwc_no_svn('foo', auth=self.auth) - wc.checkout('url') - assert wc.commands[0][-1] == ('--username="user" --password="pass" ' - '--no-auth-cache') - - def test_commit(self): - wc = svnwc_no_svn('foo', auth=self.auth) - wc.commit('msg') - assert wc.commands[0][-1] == ('--username="user" --password="pass" ' - '--no-auth-cache') - - def test_checkout_no_cache_auth(self): - wc = svnwc_no_svn('foo', auth=self.auth) - wc.checkout('url') - assert wc.commands[0][-1] == ('--username="user" --password="pass" ' - '--no-auth-cache') - - def test_checkout_auth_from_constructor(self): - wc = svnwc_no_svn('foo', auth=self.auth) - wc.checkout('url') - assert wc.commands[0][-1] == ('--username="user" --password="pass" ' - '--no-auth-cache') - -class svnurl_no_svn(py.path.svnurl): - cmdexec_output = 'test' - popen_output = 'test' - def __new__(cls, *args, **kwargs): - self = super(svnurl_no_svn, cls).__new__(cls, *args, **kwargs) - self.commands = [] - return self - - def _cmdexec(self, cmd): - self.commands.append(cmd) - return self.cmdexec_output - - def _popen(self, cmd): - self.commands.append(cmd) - return self.popen_output - -class TestSvnURLAuth(object): - def setup_method(self, meth): - self.auth = SvnAuth('foo', 'bar') - - def test_init(self): - u = svnurl_no_svn('http://foo.bar/svn') - assert u.auth is None - - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - assert u.auth is self.auth - - def test_new(self): - u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth) - new = u.new(basename='bar') - assert new.auth is self.auth - assert new.url == 'http://foo.bar/svn/bar' - - def test_join(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - new = u.join('foo') - assert new.auth is self.auth - assert new.url == 'http://foo.bar/svn/foo' - - def test_listdir(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - u.cmdexec_output = '''\ - 1717 johnny 1529 Nov 04 14:32 LICENSE.txt - 1716 johnny 5352 Nov 04 14:28 README.txt -''' - paths = u.listdir() - assert paths[0].auth is self.auth - assert paths[1].auth is self.auth - assert paths[0].basename == 'LICENSE.txt' - - def test_info(self): - u = svnurl_no_svn('http://foo.bar/svn/LICENSE.txt', auth=self.auth) - def dirpath(self): - return self - u.cmdexec_output = '''\ - 1717 johnny 1529 Nov 04 14:32 LICENSE.txt - 1716 johnny 5352 Nov 04 14:28 README.txt -''' - org_dp = u.__class__.dirpath - u.__class__.dirpath = dirpath - try: - info = u.info() - finally: - u.dirpath = org_dp - assert info.size == 1529 - - def test_open(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - foo = u.join('foo') - foo.check = lambda *args, **kwargs: True - ret = foo.open() - assert ret == 'test' - assert '--username="foo" --password="bar"' in foo.commands[0] - - def test_dirpath(self): - u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth) - parent = u.dirpath() - assert parent.auth is self.auth - - def test_mkdir(self): - u = svnurl_no_svn('http://foo.bar/svn/qweqwe', auth=self.auth) - assert not u.commands - u.mkdir(msg='created dir foo') - assert u.commands - assert '--username="foo" --password="bar"' in u.commands[0] - - def test_copy(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - u2 = svnurl_no_svn('http://foo.bar/svn2') - u.copy(u2, 'copied dir') - assert '--username="foo" --password="bar"' in u.commands[0] - - def test_rename(self): - u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth) - u.rename('http://foo.bar/svn/bar', 'moved foo to bar') - assert '--username="foo" --password="bar"' in u.commands[0] - - def test_remove(self): - u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth) - u.remove(msg='removing foo') - assert '--username="foo" --password="bar"' in u.commands[0] - - def test_export(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - target = py.path.local('/foo') - u.export(target) - assert '--username="foo" --password="bar"' in u.commands[0] - - def test_log(self): - u = svnurl_no_svn('http://foo.bar/svn/foo', auth=self.auth) - u.popen_output = py.io.TextIO(py.builtin._totext('''\ -<?xml version="1.0"?> -<log> -<logentry revision="51381"> -<author>guido</author> -<date>2008-02-11T12:12:18.476481Z</date> -<msg>Creating branch to work on auth support for py.path.svn*. -</msg> -</logentry> -</log> -''', 'ascii')) - u.check = lambda *args, **kwargs: True - ret = u.log(10, 20, verbose=True) - assert '--username="foo" --password="bar"' in u.commands[0] - assert len(ret) == 1 - assert int(ret[0].rev) == 51381 - assert ret[0].author == 'guido' - - def test_propget(self): - u = svnurl_no_svn('http://foo.bar/svn', auth=self.auth) - u.propget('foo') - assert '--username="foo" --password="bar"' in u.commands[0] - -def pytest_funcarg__setup(request): - return Setup(request) - -class Setup: - def __init__(self, request): - if not svnbin: - py.test.skip("svn binary required") - if not request.config.option.runslowtests: - py.test.skip('use --runslowtests to run these tests') - - tmpdir = request.getfuncargvalue("tmpdir") - repodir = tmpdir.join("repo") - py.process.cmdexec('svnadmin create %s' % repodir) - if sys.platform == 'win32': - repodir = '/' + str(repodir).replace('\\', '/') - self.repo = py.path.svnurl("file://%s" % repodir) - if sys.platform == 'win32': - # remove trailing slash... - repodir = repodir[1:] - self.repopath = py.path.local(repodir) - self.temppath = tmpdir.mkdir("temppath") - self.auth = SvnAuth('johnny', 'foo', cache_auth=False, - interactive=False) - make_repo_auth(self.repopath, {'johnny': ('foo', 'rw')}) - self.port, self.pid = serve_bg(self.repopath.dirpath()) - # XXX caching is too global - py.path.svnurl._lsnorevcache._dict.clear() - request.addfinalizer(lambda: py.process.kill(self.pid)) - -class TestSvnWCAuthFunctional: - def test_checkout_constructor_arg(self, setup): - wc = py.path.svnwc(setup.temppath, auth=setup.auth) - wc.checkout( - 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename)) - assert wc.join('.svn').check() - - def test_checkout_function_arg(self, setup): - wc = py.path.svnwc(setup.temppath, auth=setup.auth) - wc.checkout( - 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename)) - assert wc.join('.svn').check() - - def test_checkout_failing_non_interactive(self, setup): - auth = SvnAuth('johnny', 'bar', cache_auth=False, - interactive=False) - wc = py.path.svnwc(setup.temppath, auth) - py.test.raises(Exception, - ("wc.checkout('svn://localhost:%(port)s/%(repopath)s')" % - setup.__dict__)) - - def test_log(self, setup): - wc = py.path.svnwc(setup.temppath, setup.auth) - wc.checkout( - 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename)) - foo = wc.ensure('foo.txt') - wc.commit('added foo.txt') - log = foo.log() - assert len(log) == 1 - assert log[0].msg == 'added foo.txt' - - def test_switch(self, setup): - import pytest - try: - import xdist - pytest.skip('#160: fails under xdist') - except ImportError: - pass - wc = py.path.svnwc(setup.temppath, auth=setup.auth) - svnurl = 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename) - wc.checkout(svnurl) - wc.ensure('foo', dir=True).ensure('foo.txt').write('foo') - wc.commit('added foo dir with foo.txt file') - wc.ensure('bar', dir=True) - wc.commit('added bar dir') - bar = wc.join('bar') - bar.switch(svnurl + '/foo') - assert bar.join('foo.txt') - - def test_update(self, setup): - wc1 = py.path.svnwc(setup.temppath.ensure('wc1', dir=True), - auth=setup.auth) - wc2 = py.path.svnwc(setup.temppath.ensure('wc2', dir=True), - auth=setup.auth) - wc1.checkout( - 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename)) - wc2.checkout( - 'svn://localhost:%s/%s' % (setup.port, setup.repopath.basename)) - wc1.ensure('foo', dir=True) - wc1.commit('added foo dir') - wc2.update() - assert wc2.join('foo').check() - - auth = SvnAuth('unknown', 'unknown', interactive=False) - wc2.auth = auth - py.test.raises(Exception, 'wc2.update()') - - def test_lock_unlock_status(self, setup): - port = setup.port - wc = py.path.svnwc(setup.temppath, auth=setup.auth) - wc.checkout( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename,)) - wc.ensure('foo', file=True) - wc.commit('added foo file') - foo = wc.join('foo') - foo.lock() - status = foo.status() - assert status.locked - foo.unlock() - status = foo.status() - assert not status.locked - - auth = SvnAuth('unknown', 'unknown', interactive=False) - foo.auth = auth - py.test.raises(Exception, 'foo.lock()') - py.test.raises(Exception, 'foo.unlock()') - - def test_diff(self, setup): - port = setup.port - wc = py.path.svnwc(setup.temppath, auth=setup.auth) - wc.checkout( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename,)) - wc.ensure('foo', file=True) - wc.commit('added foo file') - wc.update() - rev = int(wc.status().rev) - foo = wc.join('foo') - foo.write('bar') - diff = foo.diff() - assert '\n+bar\n' in diff - foo.commit('added some content') - diff = foo.diff() - assert not diff - diff = foo.diff(rev=rev) - assert '\n+bar\n' in diff - - auth = SvnAuth('unknown', 'unknown', interactive=False) - foo.auth = auth - py.test.raises(Exception, 'foo.diff(rev=rev)') - -class TestSvnURLAuthFunctional: - def test_listdir(self, setup): - port = setup.port - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=setup.auth) - u.ensure('foo') - paths = u.listdir() - assert len(paths) == 1 - assert paths[0].auth is setup.auth - - auth = SvnAuth('foo', 'bar', interactive=False) - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=auth) - py.test.raises(Exception, 'u.listdir()') - - def test_copy(self, setup): - port = setup.port - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=setup.auth) - foo = u.mkdir('foo') - assert foo.check() - bar = u.join('bar') - foo.copy(bar) - assert bar.check() - assert bar.auth is setup.auth - - auth = SvnAuth('foo', 'bar', interactive=False) - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=auth) - foo = u.join('foo') - bar = u.join('bar') - py.test.raises(Exception, 'foo.copy(bar)') - - def test_write_read(self, setup): - port = setup.port - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=setup.auth) - foo = u.ensure('foo') - fp = foo.open() - try: - data = fp.read() - finally: - fp.close() - assert data == '' - - auth = SvnAuth('foo', 'bar', interactive=False) - u = py.path.svnurl( - 'svn://localhost:%s/%s' % (port, setup.repopath.basename), - auth=auth) - foo = u.join('foo') - py.test.raises(Exception, 'foo.open()') - - # XXX rinse, repeat... :| diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnurl.py b/tests/wpt/tests/tools/third_party/py/testing/path/test_svnurl.py deleted file mode 100644 index 15fbea5047d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnurl.py +++ /dev/null @@ -1,95 +0,0 @@ -import py -from py._path.svnurl import InfoSvnCommand -import datetime -import time -from svntestbase import CommonSvnTests - -def pytest_funcarg__path1(request): - repo, repourl, wc = request.getfuncargvalue("repowc1") - return py.path.svnurl(repourl) - -class TestSvnURLCommandPath(CommonSvnTests): - @py.test.mark.xfail - def test_load(self, path1): - super(TestSvnURLCommandPath, self).test_load(path1) - - # the following two work on jython but not in local/svnwc - def test_listdir(self, path1): - super(TestSvnURLCommandPath, self).test_listdir(path1) - def test_visit_ignore(self, path1): - super(TestSvnURLCommandPath, self).test_visit_ignore(path1) - - def test_svnurl_needs_arg(self, path1): - py.test.raises(TypeError, "py.path.svnurl()") - - def test_svnurl_does_not_accept_None_either(self, path1): - py.test.raises(Exception, "py.path.svnurl(None)") - - def test_svnurl_characters_simple(self, path1): - py.path.svnurl("svn+ssh://hello/world") - - def test_svnurl_characters_at_user(self, path1): - py.path.svnurl("http://user@host.com/some/dir") - - def test_svnurl_characters_at_path(self, path1): - py.test.raises(ValueError, 'py.path.svnurl("http://host.com/foo@bar")') - - def test_svnurl_characters_colon_port(self, path1): - py.path.svnurl("http://host.com:8080/some/dir") - - def test_svnurl_characters_tilde_end(self, path1): - py.path.svnurl("http://host.com/some/file~") - - @py.test.mark.xfail("sys.platform == 'win32'") - def test_svnurl_characters_colon_path(self, path1): - # colons are allowed on win32, because they're part of the drive - # part of an absolute path... however, they shouldn't be allowed in - # other parts, I think - py.test.raises(ValueError, 'py.path.svnurl("http://host.com/foo:bar")') - - def test_export(self, path1, tmpdir): - tmpdir = tmpdir.join("empty") - p = path1.export(tmpdir) - assert p == tmpdir # XXX should return None - n1 = [x.basename for x in tmpdir.listdir()] - n2 = [x.basename for x in path1.listdir()] - n1.sort() - n2.sort() - assert n1 == n2 - assert not p.join('.svn').check() - rev = path1.mkdir("newdir") - tmpdir.remove() - assert not tmpdir.check() - path1.new(rev=1).export(tmpdir) - for p in tmpdir.listdir(): - assert p.basename in n2 - -class TestSvnInfoCommand: - - def test_svn_1_2(self): - line = " 2256 hpk 165 Nov 24 17:55 __init__.py" - info = InfoSvnCommand(line) - now = datetime.datetime.now() - assert info.last_author == 'hpk' - assert info.created_rev == 2256 - assert info.kind == 'file' - # we don't check for the year (2006), because that depends - # on the clock correctly being setup - assert time.gmtime(info.mtime)[1:6] == (11, 24, 17, 55, 0) - assert info.size == 165 - assert info.time == info.mtime * 1000000 - - def test_svn_1_3(self): - line =" 4784 hpk 2 Jun 01 2004 __init__.py" - info = InfoSvnCommand(line) - assert info.last_author == 'hpk' - assert info.kind == 'file' - - def test_svn_1_3_b(self): - line =" 74 autoadmi Oct 06 23:59 plonesolutions.com/" - info = InfoSvnCommand(line) - assert info.last_author == 'autoadmi' - assert info.kind == 'dir' - -def test_badchars(): - py.test.raises(ValueError, "py.path.svnurl('http://host/tmp/@@@:')") diff --git a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnwc.py b/tests/wpt/tests/tools/third_party/py/testing/path/test_svnwc.py deleted file mode 100644 index c643d9983fb..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/path/test_svnwc.py +++ /dev/null @@ -1,557 +0,0 @@ -import py -import os, sys -import pytest -from py._path.svnwc import InfoSvnWCCommand, XMLWCStatus, parse_wcinfotime -from py._path import svnwc as svncommon -from svntestbase import CommonSvnTests - - -pytestmark = pytest.mark.xfail(sys.platform.startswith('win'), - reason='#161 all tests in this file are failing on Windows', - run=False) - - -def test_make_repo(path1, tmpdir): - repo = tmpdir.join("repo") - py.process.cmdexec('svnadmin create %s' % repo) - if sys.platform == 'win32': - repo = '/' + str(repo).replace('\\', '/') - repo = py.path.svnurl("file://%s" % repo) - wc = py.path.svnwc(tmpdir.join("wc")) - wc.checkout(repo) - assert wc.rev == 0 - assert len(wc.listdir()) == 0 - p = wc.join("a_file") - p.write("test file") - p.add() - rev = wc.commit("some test") - assert p.info().rev == 1 - assert rev == 1 - rev = wc.commit() - assert rev is None - -def pytest_funcarg__path1(request): - repo, repourl, wc = request.getfuncargvalue("repowc1") - return wc - -class TestWCSvnCommandPath(CommonSvnTests): - def test_status_attributes_simple(self, path1): - def assert_nochange(p): - s = p.status() - assert not s.modified - assert not s.prop_modified - assert not s.added - assert not s.deleted - assert not s.replaced - - dpath = path1.join('sampledir') - assert_nochange(path1.join('sampledir')) - assert_nochange(path1.join('samplefile')) - - def test_status_added(self, path1): - nf = path1.join('newfile') - nf.write('hello') - nf.add() - try: - s = nf.status() - assert s.added - assert not s.modified - assert not s.prop_modified - assert not s.replaced - finally: - nf.revert() - - def test_status_change(self, path1): - nf = path1.join('samplefile') - try: - nf.write(nf.read() + 'change') - s = nf.status() - assert not s.added - assert s.modified - assert not s.prop_modified - assert not s.replaced - finally: - nf.revert() - - def test_status_added_ondirectory(self, path1): - sampledir = path1.join('sampledir') - try: - t2 = sampledir.mkdir('t2') - t1 = t2.join('t1') - t1.write('test') - t1.add() - s = sampledir.status(rec=1) - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - assert t1.basename in [item.basename for item in s.added] - assert t2.basename in [item.basename for item in s.added] - finally: - t2.revert(rec=1) - t2.localpath.remove(rec=1) - - def test_status_unknown(self, path1): - t1 = path1.join('un1') - try: - t1.write('test') - s = path1.status() - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - assert t1.basename in [item.basename for item in s.unknown] - finally: - t1.localpath.remove() - - def test_status_unchanged(self, path1): - r = path1 - s = path1.status(rec=1) - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - assert r.join('samplefile').basename in [item.basename - for item in s.unchanged] - assert r.join('sampledir').basename in [item.basename - for item in s.unchanged] - assert r.join('sampledir/otherfile').basename in [item.basename - for item in s.unchanged] - - def test_status_update(self, path1): - # not a mark because the global "pytestmark" will end up overwriting a mark here - pytest.xfail("svn-1.7 has buggy 'status --xml' output") - r = path1 - try: - r.update(rev=1) - s = r.status(updates=1, rec=1) - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - import pprint - pprint.pprint(s.allpath()) - assert r.join('anotherfile').basename in [item.basename for - item in s.update_available] - #assert len(s.update_available) == 1 - finally: - r.update() - - def test_status_replaced(self, path1): - p = path1.join("samplefile") - p.remove() - p.ensure(dir=0) - try: - s = path1.status() - assert p.basename in [item.basename for item in s.replaced] - finally: - path1.revert(rec=1) - - def test_status_ignored(self, path1): - try: - d = path1.join('sampledir') - p = py.path.local(d).join('ignoredfile') - p.ensure(file=True) - s = d.status() - assert [x.basename for x in s.unknown] == ['ignoredfile'] - assert [x.basename for x in s.ignored] == [] - d.propset('svn:ignore', 'ignoredfile') - s = d.status() - assert [x.basename for x in s.unknown] == [] - assert [x.basename for x in s.ignored] == ['ignoredfile'] - finally: - path1.revert(rec=1) - - def test_status_conflict(self, path1, tmpdir): - wc = path1 - wccopy = py.path.svnwc(tmpdir.join("conflict_copy")) - wccopy.checkout(wc.url) - p = wc.ensure('conflictsamplefile', file=1) - p.write('foo') - wc.commit('added conflictsamplefile') - wccopy.update() - assert wccopy.join('conflictsamplefile').check() - p.write('bar') - wc.commit('wrote some data') - wccopy.join('conflictsamplefile').write('baz') - wccopy.update(interactive=False) - s = wccopy.status() - assert [x.basename for x in s.conflict] == ['conflictsamplefile'] - - def test_status_external(self, path1, repowc2): - otherrepo, otherrepourl, otherwc = repowc2 - d = path1.ensure('sampledir', dir=1) - try: - d.update() - d.propset('svn:externals', 'otherwc %s' % (otherwc.url,)) - d.update() - s = d.status() - assert [x.basename for x in s.external] == ['otherwc'] - assert 'otherwc' not in [x.basename for x in s.unchanged] - s = d.status(rec=1) - assert [x.basename for x in s.external] == ['otherwc'] - assert 'otherwc' in [x.basename for x in s.unchanged] - finally: - path1.revert(rec=1) - - def test_status_deleted(self, path1): - d = path1.ensure('sampledir', dir=1) - d.remove() - d.ensure(dir=1) - path1.commit() - d.ensure('deletefile', dir=0) - d.commit() - s = d.status() - assert 'deletefile' in [x.basename for x in s.unchanged] - assert not s.deleted - p = d.join('deletefile') - p.remove() - s = d.status() - assert 'deletefile' not in s.unchanged - assert [x.basename for x in s.deleted] == ['deletefile'] - - def test_status_noauthor(self, path1): - # testing for XML without author - this used to raise an exception - xml = '''\ - <entry path="/tmp/pytest-23/wc"> - <wc-status item="normal" props="none" revision="0"> - <commit revision="0"> - <date>2008-08-19T16:50:53.400198Z</date> - </commit> - </wc-status> - </entry> - ''' - XMLWCStatus.fromstring(xml, path1) - - def test_status_wrong_xml(self, path1): - # testing for XML without author - this used to raise an exception - xml = '<entry path="/home/jean/zope/venv/projectdb/parts/development-products/DataGridField">\n<wc-status item="incomplete" props="none" revision="784">\n</wc-status>\n</entry>' - st = XMLWCStatus.fromstring(xml, path1) - assert len(st.incomplete) == 1 - - def test_diff(self, path1): - p = path1 / 'anotherfile' - out = p.diff(rev=2) - assert out.find('hello') != -1 - - def test_blame(self, path1): - p = path1.join('samplepickle') - lines = p.blame() - assert sum([l[0] for l in lines]) == len(lines) - for l1, l2 in zip(p.readlines(), [l[2] for l in lines]): - assert l1 == l2 - assert [l[1] for l in lines] == ['hpk'] * len(lines) - p = path1.join('samplefile') - lines = p.blame() - assert sum([l[0] for l in lines]) == len(lines) - for l1, l2 in zip(p.readlines(), [l[2] for l in lines]): - assert l1 == l2 - assert [l[1] for l in lines] == ['hpk'] * len(lines) - - def test_join_abs(self, path1): - s = str(path1.localpath) - n = path1.join(s, abs=1) - assert path1 == n - - def test_join_abs2(self, path1): - assert path1.join('samplefile', abs=1) == path1.join('samplefile') - - def test_str_gives_localpath(self, path1): - assert str(path1) == str(path1.localpath) - - def test_versioned(self, path1): - assert path1.check(versioned=1) - # TODO: Why does my copy of svn think .svn is versioned? - #assert path1.join('.svn').check(versioned=0) - assert path1.join('samplefile').check(versioned=1) - assert not path1.join('notexisting').check(versioned=1) - notexisting = path1.join('hello').localpath - try: - notexisting.write("") - assert path1.join('hello').check(versioned=0) - finally: - notexisting.remove() - - def test_listdir_versioned(self, path1): - assert path1.check(versioned=1) - p = path1.localpath.ensure("not_a_versioned_file") - l = [x.localpath - for x in path1.listdir(lambda x: x.check(versioned=True))] - assert p not in l - - def test_nonversioned_remove(self, path1): - assert path1.check(versioned=1) - somefile = path1.join('nonversioned/somefile') - nonwc = py.path.local(somefile) - nonwc.ensure() - assert somefile.check() - assert not somefile.check(versioned=True) - somefile.remove() # this used to fail because it tried to 'svn rm' - - def test_properties(self, path1): - try: - path1.propset('gaga', 'this') - assert path1.propget('gaga') == 'this' - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - assert path1.basename in [item.basename for item in - path1.status().prop_modified] - assert 'gaga' in path1.proplist() - assert path1.proplist()['gaga'] == 'this' - - finally: - path1.propdel('gaga') - - def test_proplist_recursive(self, path1): - s = path1.join('samplefile') - s.propset('gugu', 'that') - try: - p = path1.proplist(rec=1) - # Comparing just the file names, because paths are unpredictable - # on Windows. (long vs. 8.3 paths) - assert (path1 / 'samplefile').basename in [item.basename - for item in p] - finally: - s.propdel('gugu') - - def test_long_properties(self, path1): - value = """ - vadm:posix : root root 0100755 - Properties on 'chroot/dns/var/bind/db.net.xots': - """ - try: - path1.propset('gaga', value) - backvalue = path1.propget('gaga') - assert backvalue == value - #assert len(backvalue.split('\n')) == 1 - finally: - path1.propdel('gaga') - - - def test_ensure(self, path1): - newpath = path1.ensure('a', 'b', 'c') - try: - assert newpath.check(exists=1, versioned=1) - newpath.write("hello") - newpath.ensure() - assert newpath.read() == "hello" - finally: - path1.join('a').remove(force=1) - - def test_not_versioned(self, path1): - p = path1.localpath.mkdir('whatever') - f = path1.localpath.ensure('testcreatedfile') - try: - assert path1.join('whatever').check(versioned=0) - assert path1.join('testcreatedfile').check(versioned=0) - assert not path1.join('testcreatedfile').check(versioned=1) - finally: - p.remove(rec=1) - f.remove() - - def test_lock_unlock(self, path1): - root = path1 - somefile = root.join('somefile') - somefile.ensure(file=True) - # not yet added to repo - py.test.raises(Exception, 'somefile.lock()') - somefile.write('foo') - somefile.commit('test') - assert somefile.check(versioned=True) - somefile.lock() - try: - locked = root.status().locked - assert len(locked) == 1 - assert locked[0].basename == somefile.basename - assert locked[0].dirpath().basename == somefile.dirpath().basename - #assert somefile.locked() - py.test.raises(Exception, 'somefile.lock()') - finally: - somefile.unlock() - #assert not somefile.locked() - locked = root.status().locked - assert locked == [] - py.test.raises(Exception, 'somefile,unlock()') - somefile.remove() - - def test_commit_nonrecursive(self, path1): - somedir = path1.join('sampledir') - somedir.mkdir("subsubdir") - somedir.propset('foo', 'bar') - status = somedir.status() - assert len(status.prop_modified) == 1 - assert len(status.added) == 1 - - somedir.commit('non-recursive commit', rec=0) - status = somedir.status() - assert len(status.prop_modified) == 0 - assert len(status.added) == 1 - - somedir.commit('recursive commit') - status = somedir.status() - assert len(status.prop_modified) == 0 - assert len(status.added) == 0 - - def test_commit_return_value(self, path1): - testfile = path1.join('test.txt').ensure(file=True) - testfile.write('test') - rev = path1.commit('testing') - assert type(rev) == int - - anotherfile = path1.join('another.txt').ensure(file=True) - anotherfile.write('test') - rev2 = path1.commit('testing more') - assert type(rev2) == int - assert rev2 == rev + 1 - - #def test_log(self, path1): - # l = path1.log() - # assert len(l) == 3 # might need to be upped if more tests are added - -class XTestWCSvnCommandPathSpecial: - - rooturl = 'http://codespeak.net/svn/py.path/trunk/dist/py.path/test/data' - #def test_update_none_rev(self, path1): - # path = tmpdir.join('checkouttest') - # wcpath = newpath(xsvnwc=str(path), url=path1url) - # try: - # wcpath.checkout(rev=2100) - # wcpath.update() - # assert wcpath.info().rev > 2100 - # finally: - # wcpath.localpath.remove(rec=1) - -def test_parse_wcinfotime(): - assert (parse_wcinfotime('2006-05-30 20:45:26 +0200 (Tue, 30 May 2006)') == - 1149021926) - assert (parse_wcinfotime('2003-10-27 20:43:14 +0100 (Mon, 27 Oct 2003)') == - 1067287394) - -class TestInfoSvnWCCommand: - - def test_svn_1_2(self, path1): - output = """ - Path: test_svnwc.py - Name: test_svnwc.py - URL: http://codespeak.net/svn/py/dist/py/path/svn/wccommand.py - Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada - Revision: 28137 - Node Kind: file - Schedule: normal - Last Changed Author: jan - Last Changed Rev: 27939 - Last Changed Date: 2006-05-30 20:45:26 +0200 (Tue, 30 May 2006) - Text Last Updated: 2006-06-01 00:42:53 +0200 (Thu, 01 Jun 2006) - Properties Last Updated: 2006-05-23 11:54:59 +0200 (Tue, 23 May 2006) - Checksum: 357e44880e5d80157cc5fbc3ce9822e3 - """ - path = py.path.local(__file__).dirpath().chdir() - try: - info = InfoSvnWCCommand(output) - finally: - path.chdir() - assert info.last_author == 'jan' - assert info.kind == 'file' - assert info.mtime == 1149021926.0 - assert info.url == 'http://codespeak.net/svn/py/dist/py/path/svn/wccommand.py' - assert info.time == 1149021926000000.0 - assert info.rev == 28137 - - - def test_svn_1_3(self, path1): - output = """ - Path: test_svnwc.py - Name: test_svnwc.py - URL: http://codespeak.net/svn/py/dist/py/path/svn/wccommand.py - Repository Root: http://codespeak.net/svn - Repository UUID: fd0d7bf2-dfb6-0310-8d31-b7ecfe96aada - Revision: 28124 - Node Kind: file - Schedule: normal - Last Changed Author: jan - Last Changed Rev: 27939 - Last Changed Date: 2006-05-30 20:45:26 +0200 (Tue, 30 May 2006) - Text Last Updated: 2006-06-02 23:46:11 +0200 (Fri, 02 Jun 2006) - Properties Last Updated: 2006-06-02 23:45:28 +0200 (Fri, 02 Jun 2006) - Checksum: 357e44880e5d80157cc5fbc3ce9822e3 - """ - path = py.path.local(__file__).dirpath().chdir() - try: - info = InfoSvnWCCommand(output) - finally: - path.chdir() - assert info.last_author == 'jan' - assert info.kind == 'file' - assert info.mtime == 1149021926.0 - assert info.url == 'http://codespeak.net/svn/py/dist/py/path/svn/wccommand.py' - assert info.rev == 28124 - assert info.time == 1149021926000000.0 - - -def test_characters_at(): - py.test.raises(ValueError, "py.path.svnwc('/tmp/@@@:')") - -def test_characters_tilde(): - py.path.svnwc('/tmp/test~') - - -class TestRepo: - def test_trailing_slash_is_stripped(self, path1): - # XXX we need to test more normalizing properties - url = path1.join("/") - assert path1 == url - - #def test_different_revs_compare_unequal(self, path1): - # newpath = path1.new(rev=1199) - # assert newpath != path1 - - def test_exists_svn_root(self, path1): - assert path1.check() - - #def test_not_exists_rev(self, path1): - # url = path1.__class__(path1url, rev=500) - # assert url.check(exists=0) - - #def test_nonexisting_listdir_rev(self, path1): - # url = path1.__class__(path1url, rev=500) - # raises(py.error.ENOENT, url.listdir) - - #def test_newrev(self, path1): - # url = path1.new(rev=None) - # assert url.rev == None - # assert url.strpath == path1.strpath - # url = path1.new(rev=10) - # assert url.rev == 10 - - #def test_info_rev(self, path1): - # url = path1.__class__(path1url, rev=1155) - # url = url.join("samplefile") - # res = url.info() - # assert res.size > len("samplefile") and res.created_rev == 1155 - - # the following tests are easier if we have a path class - def test_repocache_simple(self, path1): - repocache = svncommon.RepoCache() - repocache.put(path1.strpath, 42) - url, rev = repocache.get(path1.join('test').strpath) - assert rev == 42 - assert url == path1.strpath - - def test_repocache_notimeout(self, path1): - repocache = svncommon.RepoCache() - repocache.timeout = 0 - repocache.put(path1.strpath, path1.rev) - url, rev = repocache.get(path1.strpath) - assert rev == -1 - assert url == path1.strpath - - def test_repocache_outdated(self, path1): - repocache = svncommon.RepoCache() - repocache.put(path1.strpath, 42, timestamp=0) - url, rev = repocache.get(path1.join('test').strpath) - assert rev == -1 - assert url == path1.strpath - - def _test_getreporev(self): - """ this test runs so slow it's usually disabled """ - old = svncommon.repositories.repos - try: - _repocache.clear() - root = path1.new(rev=-1) - url, rev = cache.repocache.get(root.strpath) - assert rev>=0 - assert url == svnrepourl - finally: - repositories.repos = old diff --git a/tests/wpt/tests/tools/third_party/py/testing/process/__init__.py b/tests/wpt/tests/tools/third_party/py/testing/process/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/process/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/wpt/tests/tools/third_party/py/testing/process/test_cmdexec.py b/tests/wpt/tests/tools/third_party/py/testing/process/test_cmdexec.py deleted file mode 100644 index 98463d906d1..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/process/test_cmdexec.py +++ /dev/null @@ -1,41 +0,0 @@ -import py -from py.process import cmdexec - -def exvalue(): - import sys - return sys.exc_info()[1] - - -class Test_exec_cmd: - def test_simple(self): - out = cmdexec('echo hallo') - assert out.strip() == 'hallo' - assert py.builtin._istext(out) - - def test_simple_newline(self): - import sys - out = cmdexec(r"""%s -c "print ('hello')" """ % sys.executable) - assert out == 'hello\n' - assert py.builtin._istext(out) - - def test_simple_error(self): - py.test.raises(cmdexec.Error, cmdexec, 'exit 1') - - def test_simple_error_exact_status(self): - try: - cmdexec('exit 1') - except cmdexec.Error: - e = exvalue() - assert e.status == 1 - assert py.builtin._istext(e.out) - assert py.builtin._istext(e.err) - - def test_err(self): - try: - cmdexec('echoqweqwe123 hallo') - raise AssertionError("command succeeded but shouldn't") - except cmdexec.Error: - e = exvalue() - assert hasattr(e, 'err') - assert hasattr(e, 'out') - assert e.err or e.out diff --git a/tests/wpt/tests/tools/third_party/py/testing/process/test_forkedfunc.py b/tests/wpt/tests/tools/third_party/py/testing/process/test_forkedfunc.py deleted file mode 100644 index ae0d9ab7e6d..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/process/test_forkedfunc.py +++ /dev/null @@ -1,173 +0,0 @@ -import pytest -import py, sys, os - -pytestmark = py.test.mark.skipif("not hasattr(os, 'fork')") - - -def test_waitfinish_removes_tempdir(): - ff = py.process.ForkedFunc(boxf1) - assert ff.tempdir.check() - ff.waitfinish() - assert not ff.tempdir.check() - -def test_tempdir_gets_gc_collected(monkeypatch): - monkeypatch.setattr(os, 'fork', lambda: os.getpid()) - ff = py.process.ForkedFunc(boxf1) - assert ff.tempdir.check() - ff.__del__() - assert not ff.tempdir.check() - -def test_basic_forkedfunc(): - result = py.process.ForkedFunc(boxf1).waitfinish() - assert result.out == "some out\n" - assert result.err == "some err\n" - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 1 - -def test_exitstatus(): - def func(): - os._exit(4) - result = py.process.ForkedFunc(func).waitfinish() - assert result.exitstatus == 4 - assert result.signal == 0 - assert not result.out - assert not result.err - -def test_execption_in_func(): - def fun(): - raise ValueError(42) - ff = py.process.ForkedFunc(fun) - result = ff.waitfinish() - assert result.exitstatus == ff.EXITSTATUS_EXCEPTION - assert result.err.find("ValueError: 42") != -1 - assert result.signal == 0 - assert not result.retval - -def test_forkedfunc_on_fds(): - result = py.process.ForkedFunc(boxf2).waitfinish() - assert result.out == "someout" - assert result.err == "someerr" - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 2 - -def test_forkedfunc_on_fds_output(): - result = py.process.ForkedFunc(boxf3).waitfinish() - assert result.signal == 11 - assert result.out == "s" - - -def test_forkedfunc_on_stdout(): - def boxf3(): - import sys - sys.stdout.write("hello\n") - os.kill(os.getpid(), 11) - result = py.process.ForkedFunc(boxf3).waitfinish() - assert result.signal == 11 - assert result.out == "hello\n" - -def test_forkedfunc_signal(): - result = py.process.ForkedFunc(boxseg).waitfinish() - assert result.retval is None - assert result.signal == 11 - -def test_forkedfunc_huge_data(): - result = py.process.ForkedFunc(boxhuge).waitfinish() - assert result.out - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 3 - -def test_box_seq(): - # we run many boxes with huge data, just one after another - for i in range(50): - result = py.process.ForkedFunc(boxhuge).waitfinish() - assert result.out - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 3 - -def test_box_in_a_box(): - def boxfun(): - result = py.process.ForkedFunc(boxf2).waitfinish() - print (result.out) - sys.stderr.write(result.err + "\n") - return result.retval - - result = py.process.ForkedFunc(boxfun).waitfinish() - assert result.out == "someout\n" - assert result.err == "someerr\n" - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 2 - -def test_kill_func_forked(): - class A: - pass - info = A() - import time - - def box_fun(): - time.sleep(10) # we don't want to last forever here - - ff = py.process.ForkedFunc(box_fun) - os.kill(ff.pid, 15) - result = ff.waitfinish() - assert result.signal == 15 - - -def test_hooks(monkeypatch): - def _boxed(): - return 1 - - def _on_start(): - sys.stdout.write("some out\n") - sys.stdout.flush() - - def _on_exit(): - sys.stderr.write("some err\n") - sys.stderr.flush() - - result = py.process.ForkedFunc(_boxed, child_on_start=_on_start, - child_on_exit=_on_exit).waitfinish() - assert result.out == "some out\n" - assert result.err == "some err\n" - assert result.exitstatus == 0 - assert result.signal == 0 - assert result.retval == 1 - - -# ====================================================================== -# examples -# ====================================================================== -# - -def boxf1(): - sys.stdout.write("some out\n") - sys.stderr.write("some err\n") - return 1 - -def boxf2(): - os.write(1, "someout".encode('ascii')) - os.write(2, "someerr".encode('ascii')) - return 2 - -def boxf3(): - os.write(1, "s".encode('ascii')) - os.kill(os.getpid(), 11) - -def boxseg(): - os.kill(os.getpid(), 11) - -def boxhuge(): - s = " ".encode('ascii') - os.write(1, s * 10000) - os.write(2, s * 10000) - os.write(1, s * 10000) - - os.write(1, s * 10000) - os.write(2, s * 10000) - os.write(2, s * 10000) - os.write(1, s * 10000) - return 3 diff --git a/tests/wpt/tests/tools/third_party/py/testing/process/test_killproc.py b/tests/wpt/tests/tools/third_party/py/testing/process/test_killproc.py deleted file mode 100644 index b0d6e2f5153..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/process/test_killproc.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import sys -import py - - -@pytest.mark.skipif("sys.platform.startswith('java')") -def test_kill(tmpdir): - subprocess = pytest.importorskip("subprocess") - t = tmpdir.join("t.py") - t.write("import time ; time.sleep(100)") - proc = subprocess.Popen([sys.executable, str(t)]) - assert proc.poll() is None # no return value yet - py.process.kill(proc.pid) - ret = proc.wait() - if sys.platform == "win32" and ret == 0: - pytest.skip("XXX on win32, subprocess.Popen().wait() on a killed " - "process does not yield return value != 0") - assert ret != 0 diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/__init__.py b/tests/wpt/tests/tools/third_party/py/testing/root/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/test_builtin.py b/tests/wpt/tests/tools/third_party/py/testing/root/test_builtin.py deleted file mode 100644 index 287c60d552a..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/test_builtin.py +++ /dev/null @@ -1,156 +0,0 @@ -import sys -import types -import py -from py.builtin import set, frozenset - -def test_enumerate(): - l = [0,1,2] - for i,x in enumerate(l): - assert i == x - -def test_any(): - assert not py.builtin.any([0,False, None]) - assert py.builtin.any([0,False, None,1]) - -def test_all(): - assert not py.builtin.all([True, 1, False]) - assert py.builtin.all([True, 1, object]) - -def test_BaseException(): - assert issubclass(IndexError, py.builtin.BaseException) - assert issubclass(Exception, py.builtin.BaseException) - assert issubclass(KeyboardInterrupt, py.builtin.BaseException) - - class MyRandomClass(object): - pass - assert not issubclass(MyRandomClass, py.builtin.BaseException) - - assert py.builtin.BaseException.__module__ in ('exceptions', 'builtins') - assert Exception.__name__ == 'Exception' - - -def test_GeneratorExit(): - assert py.builtin.GeneratorExit.__module__ in ('exceptions', 'builtins') - assert issubclass(py.builtin.GeneratorExit, py.builtin.BaseException) - -def test_reversed(): - reversed = py.builtin.reversed - r = reversed("hello") - assert iter(r) is r - s = "".join(list(r)) - assert s == "olleh" - assert list(reversed(list(reversed("hello")))) == ['h','e','l','l','o'] - py.test.raises(TypeError, reversed, reversed("hello")) - -def test_simple(): - s = set([1, 2, 3, 4]) - assert s == set([3, 4, 2, 1]) - s1 = s.union(set([5, 6])) - assert 5 in s1 - assert 1 in s1 - -def test_frozenset(): - s = set([frozenset([0, 1]), frozenset([1, 0])]) - assert len(s) == 1 - - -def test_print_simple(): - from py.builtin import print_ - py.test.raises(TypeError, "print_(hello=3)") - f = py.io.TextIO() - print_("hello", "world", file=f) - s = f.getvalue() - assert s == "hello world\n" - - f = py.io.TextIO() - print_("hello", end="", file=f) - s = f.getvalue() - assert s == "hello" - - f = py.io.TextIO() - print_("xyz", "abc", sep="", end="", file=f) - s = f.getvalue() - assert s == "xyzabc" - - class X: - def __repr__(self): return "rep" - f = py.io.TextIO() - print_(X(), file=f) - assert f.getvalue() == "rep\n" - -def test_execfile(tmpdir): - test_file = tmpdir.join("test.py") - test_file.write("x = y\ndef f(): pass") - ns = {"y" : 42} - py.builtin.execfile(str(test_file), ns) - assert ns["x"] == 42 - assert py.code.getrawcode(ns["f"]).co_filename == str(test_file) - class A: - y = 3 - x = 4 - py.builtin.execfile(str(test_file)) - assert A.x == 3 - -def test_getfuncdict(): - def f(): - raise NotImplementedError - f.x = 4 - assert py.builtin._getfuncdict(f)["x"] == 4 - assert py.builtin._getfuncdict(2) is None - -def test_callable(): - class A: pass - assert py.builtin.callable(test_callable) - assert py.builtin.callable(A) - assert py.builtin.callable(list) - assert py.builtin.callable(id) - assert not py.builtin.callable(4) - assert not py.builtin.callable("hi") - -def test_totext(): - py.builtin._totext("hello", "UTF-8") - -def test_bytes_text(): - if sys.version_info[0] < 3: - assert py.builtin.text == unicode - assert py.builtin.bytes == str - else: - assert py.builtin.text == str - assert py.builtin.bytes == bytes - -def test_totext_badutf8(): - # this was in printouts within the pytest testsuite - # totext would fail - if sys.version_info >= (3,): - errors = 'surrogateescape' - else: # old python has crappy error handlers - errors = 'replace' - py.builtin._totext("\xa6", "UTF-8", errors) - -def test_reraise(): - from py.builtin import _reraise - try: - raise Exception() - except Exception: - cls, val, tb = sys.exc_info() - excinfo = py.test.raises(Exception, "_reraise(cls, val, tb)") - -def test_exec(): - l = [] - py.builtin.exec_("l.append(1)") - assert l == [1] - d = {} - py.builtin.exec_("x=4", d) - assert d['x'] == 4 - -def test_tryimport(): - py.test.raises(ImportError, py.builtin._tryimport, 'xqwe123') - x = py.builtin._tryimport('asldkajsdl', 'py') - assert x == py - x = py.builtin._tryimport('asldkajsdl', 'py.path') - assert x == py.path - -def test_getcode(): - code = py.builtin._getcode(test_getcode) - assert isinstance(code, types.CodeType) - assert py.builtin._getcode(4) is None diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/test_error.py b/tests/wpt/tests/tools/third_party/py/testing/root/test_error.py deleted file mode 100644 index 7bfbef3bd4c..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/test_error.py +++ /dev/null @@ -1,76 +0,0 @@ - -import py - -import errno -import sys -import subprocess - - -def test_error_classes(): - for name in errno.errorcode.values(): - x = getattr(py.error, name) - assert issubclass(x, py.error.Error) - assert issubclass(x, EnvironmentError) - - -def test_has_name(): - assert py.error.__name__ == 'py.error' - - -def test_picklability_issue1(): - import pickle - e1 = py.error.ENOENT() - s = pickle.dumps(e1) - e2 = pickle.loads(s) - assert isinstance(e2, py.error.ENOENT) - - -def test_unknown_error(): - num = 3999 - cls = py.error._geterrnoclass(num) - assert cls.__name__ == 'UnknownErrno%d' % (num,) - assert issubclass(cls, py.error.Error) - assert issubclass(cls, EnvironmentError) - cls2 = py.error._geterrnoclass(num) - assert cls is cls2 - - -def test_error_conversion_enotdir(testdir): - p = testdir.makepyfile("") - excinfo = py.test.raises(py.error.Error, py.error.checked_call, p.listdir) - assert isinstance(excinfo.value, EnvironmentError) - assert isinstance(excinfo.value, py.error.Error) - assert "ENOTDIR" in repr(excinfo.value) - - -def test_checked_call_supports_kwargs(tmpdir): - import tempfile - py.error.checked_call(tempfile.mkdtemp, dir=str(tmpdir)) - - -def test_error_importable(): - """Regression test for #179""" - subprocess.check_call( - [sys.executable, '-c', 'from py.error import ENOENT']) - - -try: - import unittest - unittest.TestCase.assertWarns -except (ImportError, AttributeError): - pass # required interface not available -else: - import sys - import warnings - - class Case(unittest.TestCase): - def test_assert_warns(self): - # Clear everything "py.*" from sys.modules and re-import py - # as a fresh start - for mod in tuple(sys.modules.keys()): - if mod and (mod == 'py' or mod.startswith('py.')): - del sys.modules[mod] - __import__('py') - - with self.assertWarns(UserWarning): - warnings.warn('this should work') diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/test_py_imports.py b/tests/wpt/tests/tools/third_party/py/testing/root/test_py_imports.py deleted file mode 100644 index 31fe6ead810..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/test_py_imports.py +++ /dev/null @@ -1,71 +0,0 @@ -import py -import sys - - -@py.test.mark.parametrize('name', [x for x in dir(py) if x[0] != '_']) -def test_dir(name): - obj = getattr(py, name) - if hasattr(obj, '__map__'): # isinstance(obj, Module): - keys = dir(obj) - assert len(keys) > 0 - print (obj.__map__) - for name in list(obj.__map__): - assert hasattr(obj, name), (obj, name) - - -def test_virtual_module_identity(): - from py import path as path1 - from py import path as path2 - assert path1 is path2 - from py.path import local as local1 - from py.path import local as local2 - assert local1 is local2 - - -def test_importall(): - base = py._pydir - nodirs = [ - ] - if sys.version_info >= (3, 0): - nodirs.append(base.join('_code', '_assertionold.py')) - else: - nodirs.append(base.join('_code', '_assertionnew.py')) - - def recurse(p): - return p.check(dotfile=0) and p.basename != "attic" - - for p in base.visit('*.py', recurse): - if p.basename == '__init__.py': - continue - relpath = p.new(ext='').relto(base) - if base.sep in relpath: # not py/*.py itself - for x in nodirs: - if p == x or p.relto(x): - break - else: - relpath = relpath.replace(base.sep, '.') - modpath = 'py.%s' % relpath - try: - check_import(modpath) - except py.test.skip.Exception: - pass - - -def check_import(modpath): - py.builtin.print_("checking import", modpath) - assert __import__(modpath) - - -def test_star_import(): - exec("from py import *") - - -def test_all_resolves(): - seen = py.builtin.set([py]) - lastlength = None - while len(seen) != lastlength: - lastlength = len(seen) - for item in py.builtin.frozenset(seen): - for value in item.__dict__.values(): - if isinstance(value, type(py.test)): - seen.add(value) diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/test_std.py b/tests/wpt/tests/tools/third_party/py/testing/root/test_std.py deleted file mode 100644 index 143556a0557..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/test_std.py +++ /dev/null @@ -1,13 +0,0 @@ - -import py - -def test_os(): - import os - assert py.std.os is os - -def test_import_error_converts_to_attributeerror(): - py.test.raises(AttributeError, "py.std.xyzalskdj") - -def test_std_gets_it(): - for x in py.std.sys.modules: - assert x in py.std.__dict__ diff --git a/tests/wpt/tests/tools/third_party/py/testing/root/test_xmlgen.py b/tests/wpt/tests/tools/third_party/py/testing/root/test_xmlgen.py deleted file mode 100644 index fc0e82665f7..00000000000 --- a/tests/wpt/tests/tools/third_party/py/testing/root/test_xmlgen.py +++ /dev/null @@ -1,146 +0,0 @@ - -import py -from py._xmlgen import unicode, html, raw -import sys - -class ns(py.xml.Namespace): - pass - -def test_escape(): - uvalue = py.builtin._totext('\xc4\x85\xc4\x87\n\xe2\x82\xac\n', 'utf-8') - class A: - def __unicode__(self): - return uvalue - def __str__(self): - x = self.__unicode__() - if sys.version_info[0] < 3: - return x.encode('utf-8') - return x - y = py.xml.escape(uvalue) - assert y == uvalue - x = py.xml.escape(A()) - assert x == uvalue - if sys.version_info[0] < 3: - assert isinstance(x, unicode) - assert isinstance(y, unicode) - y = py.xml.escape(uvalue.encode('utf-8')) - assert y == uvalue - - -def test_tag_with_text(): - x = ns.hello("world") - u = unicode(x) - assert u == "<hello>world</hello>" - -def test_class_identity(): - assert ns.hello is ns.hello - -def test_tag_with_text_and_attributes(): - x = ns.some(name="hello", value="world") - assert x.attr.name == 'hello' - assert x.attr.value == 'world' - u = unicode(x) - assert u == '<some name="hello" value="world"/>' - -def test_tag_with_subclassed_attr_simple(): - class my(ns.hello): - class Attr(ns.hello.Attr): - hello="world" - x = my() - assert x.attr.hello == 'world' - assert unicode(x) == '<my hello="world"/>' - -def test_tag_with_raw_attr(): - x = html.object(data=raw('&')) - assert unicode(x) == '<object data="&"></object>' - -def test_tag_nested(): - x = ns.hello(ns.world()) - unicode(x) # triggers parentifying - assert x[0].parent is x - u = unicode(x) - assert u == '<hello><world/></hello>' - -def test_list_nested(): - x = ns.hello([ns.world()]) #pass in a list here - u = unicode(x) - assert u == '<hello><world/></hello>' - -def test_tag_xmlname(): - class my(ns.hello): - xmlname = 'world' - u = unicode(my()) - assert u == '<world/>' - -def test_tag_with_text_entity(): - x = ns.hello('world & rest') - u = unicode(x) - assert u == "<hello>world & rest</hello>" - -def test_tag_with_text_and_attributes_entity(): - x = ns.some(name="hello & world") - assert x.attr.name == "hello & world" - u = unicode(x) - assert u == '<some name="hello & world"/>' - -def test_raw(): - x = ns.some(py.xml.raw("<p>literal</p>")) - u = unicode(x) - assert u == "<some><p>literal</p></some>" - - -def test_html_name_stickyness(): - class my(html.p): - pass - x = my("hello") - assert unicode(x) == '<p>hello</p>' - -def test_stylenames(): - class my: - class body(html.body): - style = html.Style(font_size = "12pt") - u = unicode(my.body()) - assert u == '<body style="font-size: 12pt"></body>' - -def test_class_None(): - t = html.body(class_=None) - u = unicode(t) - assert u == '<body></body>' - -def test_alternating_style(): - alternating = ( - html.Style(background="white"), - html.Style(background="grey"), - ) - class my(html): - class li(html.li): - def style(self): - i = self.parent.index(self) - return alternating[i%2] - style = property(style) - - x = my.ul( - my.li("hello"), - my.li("world"), - my.li("42")) - u = unicode(x) - assert u == ('<ul><li style="background: white">hello</li>' - '<li style="background: grey">world</li>' - '<li style="background: white">42</li>' - '</ul>') - -def test_singleton(): - h = html.head(html.link(href="foo")) - assert unicode(h) == '<head><link href="foo"/></head>' - - h = html.head(html.script(src="foo")) - assert unicode(h) == '<head><script src="foo"></script></head>' - -def test_inline(): - h = html.div(html.span('foo'), html.span('bar')) - assert (h.unicode(indent=2) == - '<div><span>foo</span><span>bar</span></div>') - -def test_object_tags(): - o = html.object(html.object()) - assert o.unicode(indent=0) == '<object><object></object></object>' diff --git a/tests/wpt/tests/tools/third_party/py/tox.ini b/tests/wpt/tests/tools/third_party/py/tox.ini deleted file mode 100644 index f3203507fd9..00000000000 --- a/tests/wpt/tests/tools/third_party/py/tox.ini +++ /dev/null @@ -1,44 +0,0 @@ -[tox] -# Skip py37-pytest29 as such a combination does not work (#192) -envlist=py{27,35,36}-pytest{29,30,31},py37-pytest{30,31} - -[testenv] -commands= - pip install -U . # hande the install order fallout since pytest depends on pip - py.test --confcutdir=. --junitxml={envlogdir}/junit-{envname}.xml [] -deps= - attrs - pytest29: pytest~=2.9.0 - pytest30: pytest~=3.0.0 - pytest31: pytest~=3.1.0 - -[testenv:py27-xdist] -basepython=python2.7 -deps= - pytest~=2.9.0 - pytest-xdist<=1.16.0 -commands= - pip install -U .. # hande the install order fallout since pytest depends on pip - py.test -n3 --confcutdir=.. --runslowtests \ - --junitxml={envlogdir}/junit-{envname}.xml [] - -[testenv:jython] -changedir=testing -commands= - {envpython} -m pip install -U .. # hande the install order fallout since pytest depends on pip - {envpython} -m pytest --confcutdir=.. --junitxml={envlogdir}/junit-{envname}0.xml {posargs:io_ code} - -[pytest] -rsyncdirs = conftest.py py doc testing -addopts = -ra -testpaths = testing - -[coverage:run] -branch = 1 -source = . -parallel = 1 -[coverage:report] -include = py/*,testing/* -exclude_lines = - #\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER) - ^\s*raise NotImplementedError\b diff --git a/tests/wpt/tests/tools/third_party/pytest/.git-blame-ignore-revs b/tests/wpt/tests/tools/third_party/pytest/.git-blame-ignore-revs new file mode 100644 index 00000000000..bce64a374be --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/.git-blame-ignore-revs @@ -0,0 +1,33 @@ +# List of revisions that can be ignored with git-blame(1). +# +# See `blame.ignoreRevsFile` in git-config(1) to enable it by default, or +# use it with `--ignore-revs-file` manually with git-blame. +# +# To "install" it: +# +# git config --local blame.ignoreRevsFile .gitblameignore + +# run black +703e4b11ba76171eccd3f13e723c47b810ded7ef +# switched to src layout +eaa882f3d5340956beb176aa1753e07e3f3f2190 +# pre-commit run pyupgrade --all-files +a91fe1feddbded535a4322ab854429e3a3961fb4 +# move node base classes from main to nodes +afc607cfd81458d4e4f3b1f3cf8cc931b933907e +# [?] split most fixture related code into own plugin +8c49561470708761f7321504f5e8343811be87ac +# run pyupgrade +9aacb4635e81edd6ecf281d4f6c0cfc8e94ab301 +# run blacken-docs +5f95dce95602921a70bfbc7d8de2f7712c5e4505 +# ran pyupgrade-docs again +75d0b899bbb56d6849e9d69d83a9426ed3f43f8b +# move argument parser to own file +c9df77cbd6a365dcb73c39618e4842711817e871 +# Replace reorder-python-imports by isort due to black incompatibility (#11896) +8b54596639f41dfac070030ef20394b9001fe63c +# Run blacken-docs with black's 2024's style +4546d5445aaefe6a03957db028c263521dfb5c4b +# Migration to ruff / ruff format +4588653b2497ed25976b7aaff225b889fb476756 diff --git a/tests/wpt/tests/tools/third_party/pytest/.gitblameignore b/tests/wpt/tests/tools/third_party/pytest/.gitblameignore deleted file mode 100644 index 0cb298b024d..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/.gitblameignore +++ /dev/null @@ -1,28 +0,0 @@ -# List of revisions that can be ignored with git-blame(1). -# -# See `blame.ignoreRevsFile` in git-config(1) to enable it by default, or -# use it with `--ignore-revs-file` manually with git-blame. -# -# To "install" it: -# -# git config --local blame.ignoreRevsFile .gitblameignore - -# run black -703e4b11ba76171eccd3f13e723c47b810ded7ef -# switched to src layout -eaa882f3d5340956beb176aa1753e07e3f3f2190 -# pre-commit run pyupgrade --all-files -a91fe1feddbded535a4322ab854429e3a3961fb4 -# move node base classes from main to nodes -afc607cfd81458d4e4f3b1f3cf8cc931b933907e -# [?] split most fixture related code into own plugin -8c49561470708761f7321504f5e8343811be87ac -# run pyupgrade -9aacb4635e81edd6ecf281d4f6c0cfc8e94ab301 -# run blacken-docs -5f95dce95602921a70bfbc7d8de2f7712c5e4505 -# ran pyupgrade-docs again -75d0b899bbb56d6849e9d69d83a9426ed3f43f8b - -# move argument parser to own file -c9df77cbd6a365dcb73c39618e4842711817e871 diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/dependabot.yml b/tests/wpt/tests/tools/third_party/pytest/.github/dependabot.yml index 507789bf5a4..294b13743e2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.github/dependabot.yml +++ b/tests/wpt/tests/tools/third_party/pytest/.github/dependabot.yml @@ -9,3 +9,9 @@ updates: allow: - dependency-type: direct - dependency-type: indirect +- package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + time: "03:00" + open-pull-requests-limit: 10 diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/backport.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/backport.yml new file mode 100644 index 00000000000..38ce7260278 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/backport.yml @@ -0,0 +1,51 @@ +name: backport + +on: + # Note that `pull_request_target` has security implications: + # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + # In particular: + # - Only allow triggers that can be used only be trusted users + # - Don't execute any code from the target branch + # - Don't use cache + pull_request_target: + types: [labeled] + +# Set permissions at the job level. +permissions: {} + +jobs: + backport: + if: startsWith(github.event.label.name, 'backport ') && github.event.pull_request.merged + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + + - name: Create backport PR + run: | + set -eux + + git config --global user.name "pytest bot" + git config --global user.email "pytestbot@gmail.com" + + label='${{ github.event.label.name }}' + target_branch="${label#backport }" + backport_branch=backport-${{ github.event.number }}-to-"${target_branch}" + subject="[$target_branch] $(gh pr view --json title -q .title ${{ github.event.number }})" + + git checkout origin/"${target_branch}" -b "${backport_branch}" + git cherry-pick -x --mainline 1 ${{ github.event.pull_request.merge_commit_sha }} + git commit --amend --message "$subject" + git push --set-upstream origin --force-with-lease "${backport_branch}" + gh pr create \ + --base "${target_branch}" \ + --title "${subject}" \ + --body "Backport of PR #${{ github.event.number }} to $target_branch branch. PR created by backport workflow." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/deploy.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/deploy.yml new file mode 100644 index 00000000000..20a72270fde --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/deploy.yml @@ -0,0 +1,108 @@ +name: deploy + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version' + required: true + default: '1.2.3' + + +# Set permissions at the job level. +permissions: {} + +jobs: + package: + runs-on: ubuntu-latest + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }} + timeout-minutes: 10 + + # Required by attest-build-provenance-github. + permissions: + id-token: write + attestations: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Build and Check Package + uses: hynek/build-and-inspect-python-package@v2.5.0 + with: + attest-build-provenance-github: 'true' + + deploy: + if: github.repository == 'pytest-dev/pytest' + needs: [package] + runs-on: ubuntu-latest + environment: deploy + timeout-minutes: 30 + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download Package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.14 + + - name: Push tag + run: | + git config user.name "pytest bot" + git config user.email "pytestbot@gmail.com" + git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }} + git push origin ${{ github.event.inputs.version }} + + release-notes: + + # todo: generate the content in the build job + # the goal being of using a github action script to push the release data + # after success instead of creating a complete python/tox env + needs: [deploy] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Download Package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install --upgrade tox + + - name: Generate release notes + run: | + sudo apt-get install pandoc + tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + body_path: scripts/latest-release-notes.md + files: dist/* + tag_name: ${{ github.event.inputs.version }} diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/main.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/main.yml deleted file mode 100644 index 42759ce853f..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/main.yml +++ /dev/null @@ -1,231 +0,0 @@ -name: main - -on: - push: - branches: - - main - - "[0-9]+.[0-9]+.x" - tags: - - "[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" - - pull_request: - branches: - - main - - "[0-9]+.[0-9]+.x" - -env: - PYTEST_ADDOPTS: "--color=yes" - -# Set permissions at the job level. -permissions: {} - -jobs: - build: - runs-on: ${{ matrix.os }} - timeout-minutes: 45 - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - name: [ - "windows-py36", - "windows-py37", - "windows-py37-pluggy", - "windows-py38", - "windows-py39", - "windows-py310", - "windows-py311", - - "ubuntu-py36", - "ubuntu-py37", - "ubuntu-py37-pluggy", - "ubuntu-py37-freeze", - "ubuntu-py38", - "ubuntu-py39", - "ubuntu-py310", - "ubuntu-py311", - "ubuntu-pypy3", - - "macos-py37", - "macos-py38", - - "docs", - "doctesting", - "plugins", - ] - - include: - - name: "windows-py36" - python: "3.6" - os: windows-latest - tox_env: "py36-xdist" - - name: "windows-py37" - python: "3.7" - os: windows-latest - tox_env: "py37-numpy" - - name: "windows-py37-pluggy" - python: "3.7" - os: windows-latest - tox_env: "py37-pluggymain-xdist" - - name: "windows-py38" - python: "3.8" - os: windows-latest - tox_env: "py38-unittestextras" - use_coverage: true - - name: "windows-py39" - python: "3.9" - os: windows-latest - tox_env: "py39-xdist" - - name: "windows-py310" - python: "3.10" - os: windows-latest - tox_env: "py310-xdist" - - name: "windows-py311" - python: "3.11-dev" - os: windows-latest - tox_env: "py311" - - - name: "ubuntu-py36" - python: "3.6" - os: ubuntu-latest - tox_env: "py36-xdist" - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-lsof-numpy-pexpect" - use_coverage: true - - name: "ubuntu-py37-pluggy" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-pluggymain-xdist" - - name: "ubuntu-py37-freeze" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-freeze" - - name: "ubuntu-py38" - python: "3.8" - os: ubuntu-latest - tox_env: "py38-xdist" - - name: "ubuntu-py39" - python: "3.9" - os: ubuntu-latest - tox_env: "py39-xdist" - - name: "ubuntu-py310" - python: "3.10" - os: ubuntu-latest - tox_env: "py310-xdist" - - name: "ubuntu-py311" - python: "3.11-dev" - os: ubuntu-latest - tox_env: "py311" - - name: "ubuntu-pypy3" - python: "pypy-3.7" - os: ubuntu-latest - tox_env: "pypy3-xdist" - - - name: "macos-py37" - python: "3.7" - os: macos-latest - tox_env: "py37-xdist" - - name: "macos-py38" - python: "3.8" - os: macos-latest - tox_env: "py38-xdist" - use_coverage: true - - - name: "plugins" - python: "3.7" - os: ubuntu-latest - tox_env: "plugins" - - - name: "docs" - python: "3.7" - os: ubuntu-latest - tox_env: "docs" - - name: "doctesting" - python: "3.7" - os: ubuntu-latest - tox_env: "doctesting" - use_coverage: true - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox coverage - - - name: Test without coverage - if: "! matrix.use_coverage" - run: "tox -e ${{ matrix.tox_env }}" - - - name: Test with coverage - if: "matrix.use_coverage" - run: "tox -e ${{ matrix.tox_env }}-coverage" - - - name: Generate coverage report - if: "matrix.use_coverage" - run: python -m coverage xml - - - name: Upload coverage to Codecov - if: "matrix.use_coverage" - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: true - files: ./coverage.xml - verbose: true - - deploy: - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' - - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - - needs: [build] - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.7" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade build tox - - - name: Build package - run: | - python -m build - - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_token }} - - - name: Publish GitHub release notes - env: - GH_RELEASE_NOTES_TOKEN: ${{ github.token }} - run: | - sudo apt-get install pandoc - tox -e publish-gh-release-notes diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml index 429834b3f21..1bb23fab844 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/prepare-release-pr.yml @@ -27,12 +27,12 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.8" diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/stale.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/stale.yml new file mode 100644 index 00000000000..82f9a1f2579 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/stale.yml @@ -0,0 +1,23 @@ +name: close needs-information issues +on: + schedule: + - cron: "30 1 * * *" + workflow_dispatch: + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/stale@v9 + with: + debug-only: false + days-before-issue-stale: 14 + days-before-issue-close: 7 + only-labels: "status: needs information" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days." + close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/test.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/test.yml new file mode 100644 index 00000000000..09d37aaa2c8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/test.yml @@ -0,0 +1,229 @@ +name: test + +on: + push: + branches: + - main + - "[0-9]+.[0-9]+.x" + - "test-me-*" + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" + + pull_request: + branches: + - main + - "[0-9]+.[0-9]+.x" + +env: + PYTEST_ADDOPTS: "--color=yes" + +# Cancel running jobs for the same workflow and branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Set permissions at the job level. +permissions: {} + +jobs: + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - name: Build and Check Package + uses: hynek/build-and-inspect-python-package@v2.5.0 + + build: + needs: [package] + + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + name: [ + "windows-py38", + "windows-py38-pluggy", + "windows-py39", + "windows-py310", + "windows-py311", + "windows-py312", + "windows-py313", + + "ubuntu-py38", + "ubuntu-py38-pluggy", + "ubuntu-py38-freeze", + "ubuntu-py39", + "ubuntu-py310", + "ubuntu-py311", + "ubuntu-py312", + "ubuntu-py313", + "ubuntu-pypy3", + + "macos-py38", + "macos-py39", + "macos-py310", + "macos-py312", + "macos-py313", + + "doctesting", + "plugins", + ] + + include: + - name: "windows-py38" + python: "3.8" + os: windows-latest + tox_env: "py38-unittestextras" + use_coverage: true + - name: "windows-py38-pluggy" + python: "3.8" + os: windows-latest + tox_env: "py38-pluggymain-pylib-xdist" + - name: "windows-py39" + python: "3.9" + os: windows-latest + tox_env: "py39-xdist" + - name: "windows-py310" + python: "3.10" + os: windows-latest + tox_env: "py310-xdist" + - name: "windows-py311" + python: "3.11" + os: windows-latest + tox_env: "py311" + - name: "windows-py312" + python: "3.12" + os: windows-latest + tox_env: "py312" + - name: "windows-py313" + python: "3.13-dev" + os: windows-latest + tox_env: "py313" + + - name: "ubuntu-py38" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-lsof-numpy-pexpect" + use_coverage: true + - name: "ubuntu-py38-pluggy" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-pluggymain-pylib-xdist" + - name: "ubuntu-py38-freeze" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-freeze" + - name: "ubuntu-py39" + python: "3.9" + os: ubuntu-latest + tox_env: "py39-xdist" + - name: "ubuntu-py310" + python: "3.10" + os: ubuntu-latest + tox_env: "py310-xdist" + - name: "ubuntu-py311" + python: "3.11" + os: ubuntu-latest + tox_env: "py311" + use_coverage: true + - name: "ubuntu-py312" + python: "3.12" + os: ubuntu-latest + tox_env: "py312" + use_coverage: true + - name: "ubuntu-py313" + python: "3.13-dev" + os: ubuntu-latest + tox_env: "py313" + use_coverage: true + - name: "ubuntu-pypy3" + python: "pypy-3.8" + os: ubuntu-latest + tox_env: "pypy3-xdist" + + - name: "macos-py38" + python: "3.8" + os: macos-latest + tox_env: "py38-xdist" + - name: "macos-py39" + python: "3.9" + os: macos-latest + tox_env: "py39-xdist" + use_coverage: true + - name: "macos-py310" + python: "3.10" + os: macos-latest + tox_env: "py310-xdist" + - name: "macos-py312" + python: "3.12" + os: macos-latest + tox_env: "py312-xdist" + - name: "macos-py313" + python: "3.13-dev" + os: macos-latest + tox_env: "py313-xdist" + + - name: "plugins" + python: "3.12" + os: ubuntu-latest + tox_env: "plugins" + + - name: "doctesting" + python: "3.8" + os: ubuntu-latest + tox_env: "doctesting" + use_coverage: true + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Download Package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + check-latest: ${{ endsWith(matrix.python, '-dev') }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox coverage + + - name: Test without coverage + if: "! matrix.use_coverage" + shell: bash + run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz` + + - name: Test with coverage + if: "matrix.use_coverage" + shell: bash + run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz` + + - name: Generate coverage report + if: "matrix.use_coverage" + run: python -m coverage xml + + - name: Upload coverage to Codecov + if: "matrix.use_coverage" + uses: codecov/codecov-action@v4 + continue-on-error: true + with: + fail_ci_if_error: true + files: ./coverage.xml + verbose: true diff --git a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml index 193469072ff..6943e207608 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml +++ b/tests/wpt/tests/tools/third_party/pytest/.github/workflows/update-plugin-list.yml @@ -11,7 +11,7 @@ on: permissions: {} jobs: - createPullRequest: + update-plugin-list: if: github.repository_owner == 'pytest-dev' runs-on: ubuntu-latest permissions: @@ -20,25 +20,33 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.11" + cache: pip + - name: requests-cache + uses: actions/cache@v4 + with: + path: ~/.cache/pytest-plugin-list/ + key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well + restore-keys: plugins-http-cache- - name: Install dependencies run: | python -m pip install --upgrade pip - pip install packaging requests tabulate[widechars] tqdm + pip install packaging requests tabulate[widechars] tqdm requests-cache platformdirs + - name: Update Plugin List run: python scripts/update-plugin-list.py - name: Create Pull Request - uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6 + uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 with: commit-message: '[automated] Update plugin list' author: 'pytest bot <pytestbot@users.noreply.github.com>' diff --git a/tests/wpt/tests/tools/third_party/pytest/.gitignore b/tests/wpt/tests/tools/third_party/pytest/.gitignore index 935da3b9a2e..9fccf93f7c3 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.gitignore +++ b/tests/wpt/tests/tools/third_party/pytest/.gitignore @@ -50,6 +50,8 @@ coverage.xml .project .settings .vscode +__pycache__/ +.python-version # generated by pip pip-wheel-metadata/ diff --git a/tests/wpt/tests/tools/third_party/pytest/.pre-commit-config.yaml b/tests/wpt/tests/tools/third_party/pytest/.pre-commit-config.yaml index 20cede3b7bb..a80edd28cdc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.pre-commit-config.yaml +++ b/tests/wpt/tests/tools/third_party/pytest/.pre-commit-config.yaml @@ -1,16 +1,12 @@ repos: -- repo: https://github.com/psf/black - rev: 21.11b1 - hooks: - - id: black - args: [--safe, --quiet] -- repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 - hooks: - - id: blacken-docs - additional_dependencies: [black==20.8b1] +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.4.1" + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -20,49 +16,47 @@ repos: - id: debug-statements exclude: _pytest/(debugging|hookspec).py language_version: python3 -- repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - language_version: python3 - additional_dependencies: - - flake8-typing-imports==1.9.0 - - flake8-docstrings==1.5.0 -- repo: https://github.com/asottile/reorder_python_imports - rev: v2.6.0 +- repo: https://github.com/adamchainz/blacken-docs + rev: 1.16.0 hooks: - - id: reorder-python-imports - args: ['--application-directories=.:src', --py36-plus] -- repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 - hooks: - - id: pyupgrade - args: [--py36-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 - hooks: - - id: setup-cfg-fmt - args: [--max-py-version=3.10] + - id: blacken-docs + additional_dependencies: [black==24.1.1] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v1.9.0 hooks: - id: mypy - files: ^(src/|testing/) + files: ^(src/|testing/|scripts/) args: [] additional_dependencies: - iniconfig>=1.1.0 - - py>=1.8.2 - attrs>=19.2.0 + - pluggy>=1.5.0 - packaging - tomli - - types-atomicwrites - types-pkg_resources + - types-tabulate + # for mypy running on python>=3.11 since exceptiongroup is only a dependency + # on <3.11 + - exceptiongroup>=1.0.0rc8 +- repo: https://github.com/tox-dev/pyproject-fmt + rev: "1.8.0" + hooks: + - id: pyproject-fmt + # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version + additional_dependencies: ["tox>=4.9"] - repo: local hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: ["-rn", "-sn", "--fail-on=I"] + stages: [manual] - id: rst name: rst entry: rst-lint --encoding utf-8 @@ -93,7 +87,7 @@ repos: types: [python] - id: py-path-deprecated name: py.path usage is deprecated - exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py + exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py|src/_pytest/legacypath.py language: pygrep entry: \bpy\.path\.local types: [python] diff --git a/tests/wpt/tests/tools/third_party/pytest/.readthedocs.yml b/tests/wpt/tests/tools/third_party/pytest/.readthedocs.yml index bc44d38b4c7..266d4e07aea 100644 --- a/tests/wpt/tests/tools/third_party/pytest/.readthedocs.yml +++ b/tests/wpt/tests/tools/third_party/pytest/.readthedocs.yml @@ -2,9 +2,16 @@ version: 2 python: install: - - requirements: doc/en/requirements.txt - - method: pip - path: . + # Install pytest first, then doc/en/requirements.txt. + # This order is important to honor any pins in doc/en/requirements.txt + # when the pinned library is also a dependency of pytest. + - method: pip + path: . + - requirements: doc/en/requirements.txt + +sphinx: + configuration: doc/en/conf.py + fail_on_warning: true build: os: ubuntu-20.04 diff --git a/tests/wpt/tests/tools/third_party/pytest/AUTHORS b/tests/wpt/tests/tools/third_party/pytest/AUTHORS index 9413f9c2e74..54ed85fc732 100644 --- a/tests/wpt/tests/tools/third_party/pytest/AUTHORS +++ b/tests/wpt/tests/tools/third_party/pytest/AUTHORS @@ -8,13 +8,19 @@ Abdeali JK Abdelrahman Elbehery Abhijeet Kasurde Adam Johnson +Adam Stewart Adam Uhlir Ahn Ki-Wook +Akhilesh Ramakrishnan Akiomi Kamakura Alan Velasco +Alessio Izzo +Alex Jones +Alex Lambson Alexander Johnson Alexander King Alexei Kozlenok +Alice Purcell Allan Feldman Aly Sivji Amir Elkess @@ -30,6 +36,7 @@ Andrey Paramonov Andrzej Klajnert Andrzej Ostrowski Andy Freeland +Anita Hammer Anthon van der Neut Anthony Shaw Anthony Sottile @@ -42,19 +49,28 @@ Ariel Pillemer Armin Rigo Aron Coyle Aron Curzon +Arthur Richard +Ashish Kurmi Aviral Verma Aviv Palivoda +Babak Keyvani Barney Gale +Ben Brown Ben Gartner +Ben Leith Ben Webb Benjamin Peterson +Benjamin Schubert Bernard Pratz +Bo Wu Bob Ippolito Brian Dorsey +Brian Larsen Brian Maissy Brian Okken Brianna Laugher Bruno Oliveira +Cal Jacobson Cal Leeming Carl Friedrich Bolz Carlos Jenkins @@ -62,9 +78,12 @@ Ceridwen Charles Cloud Charles Machalow Charnjit SiNGH (CCSJ) +Cheuk Ting Ho +Chris Mahoney Chris Lamb Chris NeJame Chris Rose +Chris Wheeler Christian Boelsen Christian Fetzer Christian Neumüller @@ -76,13 +95,17 @@ Christopher Dignam Christopher Gilling Claire Cecil Claudio Madotto +Clément M.T. Robert CrazyMerlyn Cristian Vera Cyrus Maden Damian Skrzypczak Daniel Grana Daniel Hahler +Daniel Miller Daniel Nuri +Daniel Sánchez Castelló +Daniel Valenzuela Zenteno Daniel Wandschneider Daniele Procida Danielle Jenkins @@ -97,6 +120,7 @@ Daw-Ran Liou Debi Mishra Denis Kirisov Denivy Braiam Rück +Dheeraj C K Dhiren Serai Diego Russo Dmitry Dygalo @@ -107,6 +131,8 @@ Edison Gustavo Muenz Edoardo Batini Edson Tadeu M. Manoel Eduardo Schettino +Edward Haigh +Eero Vaher Eli Boyarski Elizaveta Shashkova Éloi Rivard @@ -114,16 +140,24 @@ Endre Galaczi Eric Hunsberger Eric Liu Eric Siegerman +Eric Yuan Erik Aronesty +Erik Hasse Erik M. Bray Evan Kepner +Evgeny Seliverstov +Fabian Sturm Fabien Zarifian Fabio Zadrozny +faph +Felix Hofstätter Felix Nieuwenhuizen Feng Ma Florian Bruhin Florian Dahlitz Floris Bruynooghe +Fraser Stark +Gabriel Landau Gabriel Reis Garvit Shubham Gene Wood @@ -149,8 +183,12 @@ Ian Bicking Ian Lesperance Ilya Konstantinov Ionuț Turturică +Isaac Virshup +Israel Fruchter +Itxaso Aizpurua Iwan Briquemont Jaap Broekhuizen +Jake VanderPlas Jakob van Santen Jakub Mitoraj James Bourbeau @@ -162,8 +200,11 @@ Javier Romero Jeff Rackauckas Jeff Widman Jenni Rinker +Jens Tröger John Eddie Ayson +John Litborn John Towler +Jon Parise Jon Sonesen Jonas Obrist Jordan Guymon @@ -173,26 +214,32 @@ Joseph Hunkeler Josh Karpel Joshua Bronson Jurko Gospodnetić -Justyna Janczyszyn Justice Ndou +Justyna Janczyszyn Kale Kundert Kamran Ahmad +Kenny Y Karl O. Pinc Karthikeyan Singaravelan Katarzyna Jachim Katarzyna Król Katerina Koukiou Keri Volans +Kevin C Kevin Cox +Kevin Hierro Carrasco Kevin J. Foley +Kian Eliasi Kian-Meng Ang Kodi B. Arfer +Kojo Idrissa Kostis Anagnostopoulos Kristoffer Nordström Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov +Levon Saldamli Lewis Cowles Llandy Riveron Del Risco Loic Esteve @@ -203,12 +250,15 @@ Maho Maik Figura Mandeep Bhutani Manuel Krebber +Marc Mueller Marc Schlaich Marcelo Duarte Trevisani Marcin Bachry +Marc Bresson Marco Gorelli Mark Abramowitz Mark Dickinson +Marko Pacak Markus Unterwaditzer Martijn Faassen Martin Altmayer @@ -222,7 +272,6 @@ Matthias Hafner Maxim Filipenko Maximilian Cosmo Sitter mbyt -Mickey Pashov Michael Aquilina Michael Birtwell Michael Droettboom @@ -230,23 +279,30 @@ Michael Goerz Michael Krebs Michael Seifert Michal Wajszczuk +Michał Górny Michał Zięba +Mickey Pashov Mihai Capotă +Mihail Milushev Mike Hoyle (hoylemd) Mike Lundy +Milan Lesnek Miro Hrončok +mrbean-bremen Nathaniel Compton Nathaniel Waisbrot Ned Batchelder +Neil Martin Neven Mundar Nicholas Devenish Nicholas Murphy Niclas Olofsson Nicolas Delaby Nikolay Kondratyev -Olga Matoula +Nipunn Koorapati Oleg Pidsadnyi Oleg Sushchenko +Olga Matoula Oliver Bestwalter Omar Kohl Omer Hadari @@ -254,30 +310,39 @@ Ondřej Súkup Oscar Benjamin Parth Patel Patrick Hayes +Patrick Lannigan +Paul Müller +Paul Reece Pauli Virtanen Pavel Karateev Paweł Adamczak Pedro Algarvio Petter Strandmark Philipp Loose +Pierre Sassoulas Pieter Mulder Piotr Banaszkiewicz Piotr Helm +Poulami Sau Prakhar Gurunani Prashant Anand Prashant Sharma Pulkit Goyal Punyashloka Biswal Quentin Pradet +q0w Ralf Schmitt -Ram Rachum Ralph Giles +Ram Rachum Ran Benita Raphael Castaneda Raphael Pierzina +Rafal Semik Raquel Alegre Ravi Chandra +Reagan Lee Robert Holt +Roberto Aldera Roberto Polli Roland Puntaier Romain Dorgueil @@ -286,25 +351,37 @@ Ronny Pfannschmidt Ross Lawley Ruaridh Williamson Russel Winder +Russell Martin +Ryan Puddephatt Ryan Wooden +Sadra Barikbin Saiprasad Kale +Samuel Colvin Samuel Dion-Girardeau Samuel Searles-Bryant +Samuel Therrien (Avasam) Samuele Pedroni Sanket Duthade Sankt Petersbug +Saravanan Padmanaban +Sean Malloy Segev Finer Serhii Mozghovyi Seth Junot Shantanu Jain +Sharad Nair Shubham Adep +Simon Blanchard Simon Gomizelj +Simon Holesch Simon Kerr Skylar Downes Srinivas Reddy Thatiparthy +Stefaan Lippens Stefan Farmbauer Stefan Scherfke Stefan Zimmermann +Stefanie Molin Stefano Taschini Steffen Allner Stephan Obermann @@ -314,31 +391,42 @@ Tadek Teleżyński Takafumi Arakaki Taneli Hukkinen Tanvi Mehta +Tanya Agarwal Tarcisio Fischer Tareq Alayan +Tatiana Ovary Ted Xiao Terje Runde Thomas Grainger Thomas Hisch Tim Hoffmann Tim Strazny +TJ Bruno +Tobias Diez Tom Dalton Tom Viner Tomáš Gavenčiak Tomer Keren +Tony Narlock Tor Colvin Trevor Bekolay +Tushar Sadhwani Tyler Goodlet +Tyler Smart Tzu-ping Chung Vasily Kuznetsov Victor Maryama +Victor Rodriguez Victor Uriarte Vidar T. Fauske +Vijay Arora Virgil Dupras Vitaly Lashmanov +Vivaan Verma Vlad Dragos Vlad Radziuk Vladyslav Rachek +Volodymyr Kochetkov Volodymyr Piskun Wei Lin Wil Cooley @@ -348,9 +436,16 @@ Wouter van Ackooy Xixi Zhao Xuan Luong Xuecong Liao +Yannick Péroux +Yao Xiao Yoav Caspi +Yuliang Shao +Yusuke Kadowaki +Yutian Li Yuval Shimon Zac Hatfield-Dodds Zachary Kneupper +Zachary OBrien +Zhouxin Qiu Zoltán Máté Zsolt Cserna diff --git a/tests/wpt/tests/tools/third_party/pytest/CONTRIBUTING.rst b/tests/wpt/tests/tools/third_party/pytest/CONTRIBUTING.rst index 24bca723c8b..d7da59c812d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/CONTRIBUTING.rst +++ b/tests/wpt/tests/tools/third_party/pytest/CONTRIBUTING.rst @@ -50,6 +50,8 @@ Fix bugs -------- Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_. +See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_ +that are friendly to new contributors. :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going to work on a particular issue, add a comment to that effect on the specific issue. @@ -195,11 +197,12 @@ Short version ~~~~~~~~~~~~~ #. Fork the repository. +#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`). #. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed. -#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting. +#. Follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming. #. Tests are run using ``tox``:: - tox -e linting,py37 + tox -e linting,py39 The test environments above are usually enough to cover most cases locally. @@ -221,7 +224,7 @@ changes you want to review and merge. Pull requests are stored on Once you send a pull request, we can discuss its potential modifications and even add more commits to it later on. There's an excellent tutorial on how Pull Requests work in the -`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_. +`GitHub Help Center <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests>`_. Here is a simple overview, with pytest-specific bits: @@ -234,6 +237,7 @@ Here is a simple overview, with pytest-specific bits: $ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git $ cd pytest + $ git fetch --tags https://github.com/pytest-dev/pytest # now, create your own branch off "main": $ git checkout -b your-bugfix-branch-name main @@ -242,6 +246,11 @@ Here is a simple overview, with pytest-specific bits: be released in micro releases whereas features will be released in minor releases and incompatible changes in major releases. + You will need the tags to test locally, so be sure you have the tags from the main repository. If you suspect you don't, set the main repository as upstream and fetch the tags:: + + $ git remote add upstream https://github.com/pytest-dev/pytest + $ git fetch upstream --tags + If you need some help with Git, follow this quick start guide: https://git.wiki.kernel.org/index.php/QuickStart @@ -265,35 +274,35 @@ Here is a simple overview, with pytest-specific bits: #. Run all the tests - You need to have Python 3.7 available in your system. Now + You need to have Python 3.8 or later available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py37 + $ tox -e linting,py39 - This command will run tests via the "tox" tool against Python 3.7 + This command will run tests via the "tox" tool against Python 3.9 and also perform "lint" coding-style checks. -#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. +#. You can now edit your local working copy and run the tests again as necessary. Please follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming. - You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest + You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest (e.g. enter pdb on failure) to pytest you can do:: - $ tox -e py37 -- --pdb + $ tox -e py39 -- --pdb - Or to only run tests in a particular test module on Python 3.7:: + Or to only run tests in a particular test module on Python 3.9:: - $ tox -e py37 -- testing/test_config.py + $ tox -e py39 -- testing/test_config.py When committing, ``pre-commit`` will re-format the files if necessary. #. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use - an editable install with the ``testing`` extra:: + an editable install with the ``dev`` extra:: $ python3 -m venv .venv $ source .venv/bin/activate # Linux $ .venv/Scripts/activate.bat # Windows - $ pip install -e ".[testing]" + $ pip install -e ".[dev]" Afterwards, you can edit the files and run pytest normally:: @@ -378,7 +387,7 @@ them. Backporting bug fixes for the next patch release ------------------------------------------------ -Pytest makes feature release every few weeks or months. In between, patch releases +Pytest makes a feature release every few weeks or months. In between, patch releases are made to the previous feature release, containing bug fixes only. The bug fixes usually fix regressions, but may be any change that should reach users before the next feature release. @@ -387,10 +396,17 @@ Suppose for example that the latest release was 1.2.3, and you want to include a bug fix in 1.2.4 (check https://github.com/pytest-dev/pytest/releases for the actual latest release). The procedure for this is: -#. First, make sure the bug is fixed the ``main`` branch, with a regular pull +#. First, make sure the bug is fixed in the ``main`` branch, with a regular pull request, as described above. An exception to this is if the bug fix is not applicable to ``main`` anymore. +Automatic method: + +Add a ``backport 1.2.x`` label to the PR you want to backport. This will create +a backport PR against the ``1.2.x`` branch. + +Manual method: + #. ``git checkout origin/1.2.x -b backport-XXXX`` # use the main PR number here #. Locate the merge commit on the PR, in the *merged* message, for example: diff --git a/tests/wpt/tests/tools/third_party/pytest/README.rst b/tests/wpt/tests/tools/third_party/pytest/README.rst index 14733765173..a81e082cdd7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/README.rst +++ b/tests/wpt/tests/tools/third_party/pytest/README.rst @@ -20,16 +20,13 @@ :target: https://codecov.io/gh/pytest-dev/pytest :alt: Code coverage Status -.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg - :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain +.. image:: https://github.com/pytest-dev/pytest/actions/workflows/test.yml/badge.svg + :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest .. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg :target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main :alt: pre-commit.ci status -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - .. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg :target: https://www.codetriage.com/pytest-dev/pytest @@ -97,12 +94,12 @@ Features - `Modular fixtures <https://docs.pytest.org/en/stable/explanation/fixtures.html>`_ for managing small or parametrized long-lived test resources -- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial), - `nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box +- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial) + test suites out of the box -- Python 3.6+ and PyPy3 +- Python 3.8+ or PyPy3 -- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community +- Rich plugin architecture, with over 1300+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community Documentation diff --git a/tests/wpt/tests/tools/third_party/pytest/RELEASING.rst b/tests/wpt/tests/tools/third_party/pytest/RELEASING.rst index 25ce90d0f65..08004a84c00 100644 --- a/tests/wpt/tests/tools/third_party/pytest/RELEASING.rst +++ b/tests/wpt/tests/tools/third_party/pytest/RELEASING.rst @@ -37,7 +37,7 @@ breaking changes or new features. For a new minor release, first create a new maintenance branch from ``main``:: - git fetch --all + git fetch upstream git branch 7.1.x upstream/main git push upstream 7.1.x @@ -63,7 +63,7 @@ Major releases 1. Create a new maintenance branch from ``main``:: - git fetch --all + git fetch upstream git branch 8.0.x upstream/main git push upstream 8.0.x @@ -133,32 +133,32 @@ Releasing Both automatic and manual processes described above follow the same steps from this point onward. -#. After all tests pass and the PR has been approved, tag the release commit - in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI:: +#. After all tests pass and the PR has been approved, trigger the ``deploy`` job + in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch + as source. - git fetch --all - git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH - git push git@github.com:pytest-dev/pytest.git MAJOR.MINOR.PATCH + This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI + and tag the repository. - Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_. - -#. Merge the PR. +#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch. #. Cherry-pick the CHANGELOG / announce files to the ``main`` branch:: - git fetch --all --prune + git fetch upstream git checkout upstream/main -b cherry-pick-release git cherry-pick -x -m1 upstream/MAJOR.MINOR.x #. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step. -#. For major and minor releases, tag the release cherry-pick merge commit in main with +#. For major and minor releases (or the first prerelease of it), tag the release cherry-pick merge commit in main with a dev tag for the next feature release:: git checkout main git pull git tag MAJOR.{MINOR+1}.0.dev0 - git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0 + git push upstream MAJOR.{MINOR+1}.0.dev0 + +#. For major and minor releases, change the default version in the `Read the Docs Settings <https://readthedocs.org/dashboard/pytest/advanced/>`_ to the new branch. #. Send an email announcement with the contents from:: diff --git a/tests/wpt/tests/tools/third_party/pytest/TIDELIFT.rst b/tests/wpt/tests/tools/third_party/pytest/TIDELIFT.rst index 2fe25841c3a..1ba246bd868 100644 --- a/tests/wpt/tests/tools/third_party/pytest/TIDELIFT.rst +++ b/tests/wpt/tests/tools/third_party/pytest/TIDELIFT.rst @@ -23,9 +23,9 @@ members of the `contributors team`_ interested in receiving funding. The current list of contributors receiving funding are: -* `@asottile`_ * `@nicoddemus`_ * `@The-Compiler`_ +* `@RonnyPfannschmidt`_ Contributors interested in receiving a part of the funds just need to submit a PR adding their name to the list. Contributors that want to stop receiving the funds should also submit a PR @@ -55,6 +55,6 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the .. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members .. _`agreement`: https://tidelift.com/docs/lifting/agreement -.. _`@asottile`: https://github.com/asottile .. _`@nicoddemus`: https://github.com/nicoddemus .. _`@The-Compiler`: https://github.com/The-Compiler +.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt diff --git a/tests/wpt/tests/tools/third_party/pytest/bench/bench.py b/tests/wpt/tests/tools/third_party/pytest/bench/bench.py index c40fc8636c0..437d3259d83 100644 --- a/tests/wpt/tests/tools/third_party/pytest/bench/bench.py +++ b/tests/wpt/tests/tools/third_party/pytest/bench/bench.py @@ -1,10 +1,12 @@ import sys + if __name__ == "__main__": import cProfile - import pytest # NOQA import pstats + import pytest # noqa: F401 + script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"] cProfile.run("pytest.cmdline.main(%r)" % script, "prof") p = pstats.Stats("prof") diff --git a/tests/wpt/tests/tools/third_party/pytest/bench/bench_argcomplete.py b/tests/wpt/tests/tools/third_party/pytest/bench/bench_argcomplete.py index 335733df72b..459a12f9314 100644 --- a/tests/wpt/tests/tools/third_party/pytest/bench/bench_argcomplete.py +++ b/tests/wpt/tests/tools/third_party/pytest/bench/bench_argcomplete.py @@ -4,6 +4,7 @@ # FastFilesCompleter 0.7383 1.0760 import timeit + imports = [ "from argcomplete.completers import FilesCompleter as completer", "from _pytest._argcomplete import FastFilesCompleter as completer", diff --git a/tests/wpt/tests/tools/third_party/pytest/bench/skip.py b/tests/wpt/tests/tools/third_party/pytest/bench/skip.py index f0c9d1ddbef..fd5c292d92c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/bench/skip.py +++ b/tests/wpt/tests/tools/third_party/pytest/bench/skip.py @@ -1,5 +1,6 @@ import pytest + SKIP = True diff --git a/tests/wpt/tests/tools/third_party/pytest/bench/unit_test.py b/tests/wpt/tests/tools/third_party/pytest/bench/unit_test.py index ad52069dbfd..d3db111e1ae 100644 --- a/tests/wpt/tests/tools/third_party/pytest/bench/unit_test.py +++ b/tests/wpt/tests/tools/third_party/pytest/bench/unit_test.py @@ -1,5 +1,6 @@ from unittest import TestCase # noqa: F401 + for i in range(15000): exec( f""" diff --git a/tests/wpt/tests/tools/third_party/pytest/changelog/README.rst b/tests/wpt/tests/tools/third_party/pytest/changelog/README.rst index 6d026f57ef3..88956ef28d8 100644 --- a/tests/wpt/tests/tools/third_party/pytest/changelog/README.rst +++ b/tests/wpt/tests/tools/third_party/pytest/changelog/README.rst @@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where ``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of: * ``feature``: new user facing features, like new command-line options and new behavior. -* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). +* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc). * ``bugfix``: fixes a bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``deprecation``: feature deprecation. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html b/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html index 7c595e7ebf2..09d970b64ed 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/globaltoc.html @@ -17,7 +17,6 @@ <li><a href="{{ pathto('changelog') }}">Changelog</a></li> <li><a href="{{ pathto('contributing') }}">Contributing</a></li> <li><a href="{{ pathto('backwards-compatibility') }}">Backwards Compatibility</a></li> - <li><a href="{{ pathto('py27-py34-deprecation') }}">Python 2.7 and 3.4 Support</a></li> <li><a href="{{ pathto('sponsor') }}">Sponsor</a></li> <li><a href="{{ pathto('tidelift') }}">pytest for Enterprise</a></li> <li><a href="{{ pathto('license') }}">License</a></li> @@ -30,5 +29,3 @@ {%- endif %} <hr> -<a href="{{ pathto('genindex') }}">Index</a> -<hr> diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html b/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html index e98ad4ed905..f088ff8d312 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/_templates/slim_searchbox.html @@ -5,11 +5,10 @@ <div id="searchbox" style="display: none" role="search"> <div class="searchformwrapper"> <form class="search" action="{{ pathto('search') }}" method="get"> - <input type="text" name="q" aria-labelledby="searchlabel" - placeholder="Search"/> + <input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/> <input type="submit" value="{{ _('Go') }}" /> </form> </div> </div> -<script type="text/javascript">$('#searchbox').show(0);</script> +<script>document.getElementById('searchbox').style.display = "block"</script> {%- endif %} diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/adopt.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/adopt.rst index 13d82bf0116..b95a117debb 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/adopt.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/adopt.rst @@ -44,7 +44,7 @@ Partner projects, sign up here! (by 22 March) What does it mean to "adopt pytest"? ----------------------------------------- -There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right? +There can be many different definitions of "success". Pytest can run many unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right? Progressive success might look like: @@ -62,7 +62,6 @@ Progressive success might look like: It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies. -.. _nose: nose.html .. _unittest: unittest.html .. _assert: assert.html .. _pycmd: https://bitbucket.org/hpk42/pycmd/overview diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/index.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/index.rst index 9505b0b9e46..8a33f7fb57d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/index.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/index.rst @@ -6,6 +6,31 @@ Release announcements :maxdepth: 2 + release-8.2.1 + release-8.2.0 + release-8.1.2 + release-8.1.1 + release-8.1.0 + release-8.0.2 + release-8.0.1 + release-8.0.0 + release-8.0.0rc2 + release-8.0.0rc1 + release-7.4.4 + release-7.4.3 + release-7.4.2 + release-7.4.1 + release-7.4.0 + release-7.3.2 + release-7.3.1 + release-7.3.0 + release-7.2.2 + release-7.2.1 + release-7.2.0 + release-7.1.3 + release-7.1.2 + release-7.1.1 + release-7.1.0 release-7.0.1 release-7.0.0 release-7.0.0rc1 diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst index ecb1a1db988..c2a9f6da4d5 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.0.0.rst @@ -62,7 +62,7 @@ New Features - new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. -- many many more detailed improvements details +- many, many, more detailed improvements details Fixes ----------------------- @@ -109,7 +109,7 @@ Important Notes in conftest.py files. They will cause nothing special. - removed support for calling the pre-1.0 collection API of "run()" and "join" - removed reading option values from conftest.py files or env variables. - This can now be done much much better and easier through the ini-file + This can now be done much, much, better and easier through the ini-file mechanism and the "addopts" entry in particular. - removed the "disabled" attribute in test classes. Use the skipping and pytestmark mechanism to skip or xfail a test class. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst index 22ef0bc7a16..510b35ee1d0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.2.2.rst @@ -4,7 +4,7 @@ pytest-2.2.2: bug fixes pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor backward-compatible release of the versatile py.test testing tool. It contains bug fixes and a few refinements particularly to reporting with -"--collectonly", see below for betails. +"--collectonly", see below for details. For general information see here: diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst index 138cc89576c..9b864329674 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.4.0.rst @@ -181,7 +181,7 @@ Bug fixes: partially failed (finalizers would not always be called before) - fix issue320 - fix class scope for fixtures when mixed with - module-level functions. Thanks Anatloy Bubenkoff. + module-level functions. Thanks Anatoly Bubenkoff. - you can specify "-q" or "-qq" to get different levels of "quieter" reporting (thanks Katarzyna Jachim) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst index c6cdcdd8a83..fe64f1b8668 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.5.0.rst @@ -83,7 +83,7 @@ holger krekel Thanks Ralph Schmitt for the precise failure example. - fix issue244 by implementing special index for parameters to only use - indices for paramentrized test ids + indices for parametrized test ids - fix issue287 by running all finalizers but saving the exception from the first failing finalizer and re-raising it so teardown will diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst index 56fbd6cc1e4..c00df585738 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.6.0.rst @@ -73,7 +73,7 @@ holger krekel - cleanup setup.py a bit and specify supported versions. Thanks Jurko Gospodnetic for the PR. -- change XPASS colour to yellow rather then red when tests are run +- change XPASS colour to yellow rather than red when tests are run with -v. - fix issue473: work around mock putting an unbound method into a class diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst index 2840178a07f..83cddb34157 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-2.7.0.rst @@ -55,7 +55,7 @@ holger krekel github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. -- fix issue650: new option ``--docttest-ignore-import-errors`` which +- fix issue650: new option ``--doctest-ignore-import-errors`` which will turn import errors in doctests into skips. Thanks Charles Cloud for the complete PR. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.0.rst new file mode 100644 index 00000000000..3361e1c8a32 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.0.rst @@ -0,0 +1,48 @@ +pytest-7.1.0 +======================================= + +The pytest team is proud to announce the 7.1.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Akuli +* Andrew Svetlov +* Anthony Sottile +* Brett Holman +* Bruno Oliveira +* Chris NeJame +* Dan Alvizu +* Elijah DeLee +* Emmanuel Arias +* Fabian Egli +* Florian Bruhin +* Gabor Szabo +* Hasan Ramezani +* Hugo van Kemenade +* Kian Meng, Ang +* Kojo Idrissa +* Masaru Tsuchiyama +* Olga Matoula +* P. L. Lim +* Ran Benita +* Tobias Deiminger +* Yuval Shimon +* eduardo naufel schettino +* Éric + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.1.rst new file mode 100644 index 00000000000..d271c4557a2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.1.rst @@ -0,0 +1,18 @@ +pytest-7.1.1 +======================================= + +pytest 7.1.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.2.rst new file mode 100644 index 00000000000..ba33cdc694b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.2.rst @@ -0,0 +1,23 @@ +pytest-7.1.2 +======================================= + +pytest 7.1.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Anthony Sottile +* Bruno Oliveira +* Hugo van Kemenade +* Kian Eliasi +* Ran Benita +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.3.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.3.rst new file mode 100644 index 00000000000..4cb1b271704 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.1.3.rst @@ -0,0 +1,28 @@ +pytest-7.1.3 +======================================= + +pytest 7.1.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Anthony Sottile +* Bruno Oliveira +* Gergely Kalmár +* Nipunn Koorapati +* Pax +* Sviatoslav Sydorenko +* Tim Hoffmann +* Tony Narlock +* Wolfremium +* Zach OBrien +* aizpurua23a + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.0.rst new file mode 100644 index 00000000000..eca84aeb669 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.0.rst @@ -0,0 +1,93 @@ +pytest-7.2.0 +======================================= + +The pytest team is proud to announce the 7.2.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Aaron Berdy +* Adam Turner +* Albert Villanova del Moral +* Alice Purcell +* Anthony Sottile +* Anton Yakutovich +* Babak Keyvani +* Brandon Chinn +* Bruno Oliveira +* Chanvin Xiao +* Cheuk Ting Ho +* Chris Wheeler +* EmptyRabbit +* Ezio Melotti +* Florian Best +* Florian Bruhin +* Fredrik Berndtsson +* Gabriel Landau +* Gergely Kalmár +* Hugo van Kemenade +* James Gerity +* John Litborn +* Jon Parise +* Kevin C +* Kian Eliasi +* MatthewFlamm +* Miro Hrončok +* Nate Meyvis +* Neil Girdhar +* Nhieuvu1802 +* Nipunn Koorapati +* Ofek Lev +* Paul Müller +* Paul Reece +* Pax +* Pete Baughman +* Peyman Salehi +* Philipp A +* Ran Benita +* Robert O'Shea +* Ronny Pfannschmidt +* Rowin +* Ruth Comer +* Samuel Colvin +* Samuel Gaist +* Sandro Tosi +* Shantanu +* Simon K +* Stephen Rosen +* Sviatoslav Sydorenko +* Tatiana Ovary +* Thierry Moisan +* Thomas Grainger +* Tim Hoffmann +* Tobias Diez +* Tony Narlock +* Vivaan Verma +* Wolfremium +* Zac Hatfield-Dodds +* Zach OBrien +* aizpurua23a +* gresm +* holesch +* itxasos23 +* johnkangw +* skhomuti +* sommersoft +* wodny +* zx.qiu + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.1.rst new file mode 100644 index 00000000000..80ac7aff07f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.1.rst @@ -0,0 +1,25 @@ +pytest-7.2.1 +======================================= + +pytest 7.2.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Valenzuela +* Kadino +* Prerak Patel +* Ronny Pfannschmidt +* Santiago Castro +* s-padmanaban + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.2.rst new file mode 100644 index 00000000000..b34a6ff5c1e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.2.2.rst @@ -0,0 +1,25 @@ +pytest-7.2.2 +======================================= + +pytest 7.2.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Garvit Shubham +* Mahesh Vashishtha +* Ramsey +* Ronny Pfannschmidt +* Teejay +* q0w +* vin01 + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.0.rst new file mode 100644 index 00000000000..33258dabade --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.0.rst @@ -0,0 +1,130 @@ +pytest-7.3.0 +======================================= + +The pytest team is proud to announce the 7.3.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Aaron Berdy +* Adam Turner +* Albert Villanova del Moral +* Alessio Izzo +* Alex Hadley +* Alice Purcell +* Anthony Sottile +* Anton Yakutovich +* Ashish Kurmi +* Babak Keyvani +* Billy +* Brandon Chinn +* Bruno Oliveira +* Cal Jacobson +* Chanvin Xiao +* Cheuk Ting Ho +* Chris Wheeler +* Daniel Garcia Moreno +* Daniel Scheffler +* Daniel Valenzuela +* EmptyRabbit +* Ezio Melotti +* Felix Hofstätter +* Florian Best +* Florian Bruhin +* Fredrik Berndtsson +* Gabriel Landau +* Garvit Shubham +* Gergely Kalmár +* HTRafal +* Hugo van Kemenade +* Ilya Konstantinov +* Itxaso Aizpurua +* James Gerity +* Jay +* John Litborn +* Jon Parise +* Jouke Witteveen +* Kadino +* Kevin C +* Kian Eliasi +* Klaus Rettinghaus +* Kodi Arfer +* Mahesh Vashishtha +* Manuel Jacob +* Marko Pacak +* MatthewFlamm +* Miro Hrončok +* Nate Meyvis +* Neil Girdhar +* Nhieuvu1802 +* Nipunn Koorapati +* Ofek Lev +* Paul Kehrer +* Paul Müller +* Paul Reece +* Pax +* Pete Baughman +* Peyman Salehi +* Philipp A +* Pierre Sassoulas +* Prerak Patel +* Ramsey +* Ran Benita +* Robert O'Shea +* Ronny Pfannschmidt +* Rowin +* Ruth Comer +* Samuel Colvin +* Samuel Gaist +* Sandro Tosi +* Santiago Castro +* Shantanu +* Simon K +* Stefanie Molin +* Stephen Rosen +* Sviatoslav Sydorenko +* Tatiana Ovary +* Teejay +* Thierry Moisan +* Thomas Grainger +* Tim Hoffmann +* Tobias Diez +* Tony Narlock +* Vivaan Verma +* Wolfremium +* Yannick PÉROUX +* Yusuke Kadowaki +* Zac Hatfield-Dodds +* Zach OBrien +* aizpurua23a +* bitzge +* bluthej +* gresm +* holesch +* itxasos23 +* johnkangw +* q0w +* rdb +* s-padmanaban +* skhomuti +* sommersoft +* vin01 +* wim glenn +* wodny +* zx.qiu + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.1.rst new file mode 100644 index 00000000000..e920fa8af53 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.1.rst @@ -0,0 +1,18 @@ +pytest-7.3.1 +======================================= + +pytest 7.3.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.2.rst new file mode 100644 index 00000000000..b3b112f0d8e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.3.2.rst @@ -0,0 +1,21 @@ +pytest-7.3.2 +======================================= + +pytest 7.3.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Adam J. Stewart +* Alessio Izzo +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.0.rst new file mode 100644 index 00000000000..5a0d18267d3 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.0.rst @@ -0,0 +1,49 @@ +pytest-7.4.0 +======================================= + +The pytest team is proud to announce the 7.4.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Adam J. Stewart +* Alessio Izzo +* Alex +* Alex Lambson +* Brian Larsen +* Bruno Oliveira +* Bryan Ricker +* Chris Mahoney +* Facundo Batista +* Florian Bruhin +* Jarrett Keifer +* Kenny Y +* Miro Hrončok +* Ran Benita +* Roberto Aldera +* Ronny Pfannschmidt +* Sergey Kim +* Stefanie Molin +* Vijay Arora +* Ville Skyttä +* Zac Hatfield-Dodds +* bzoracler +* leeyueh +* nondescryptid +* theirix + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.1.rst new file mode 100644 index 00000000000..efadcf919e8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.1.rst @@ -0,0 +1,20 @@ +pytest-7.4.1 +======================================= + +pytest 7.4.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Florian Bruhin +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.2.rst new file mode 100644 index 00000000000..22191e7b4f9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.2.rst @@ -0,0 +1,18 @@ +pytest-7.4.2 +======================================= + +pytest 7.4.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.3.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.3.rst new file mode 100644 index 00000000000..0f319c1e7f0 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.3.rst @@ -0,0 +1,19 @@ +pytest-7.4.3 +======================================= + +pytest 7.4.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Marc Mueller + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.4.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.4.rst new file mode 100644 index 00000000000..c9633678d2e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-7.4.4.rst @@ -0,0 +1,20 @@ +pytest-7.4.4 +======================================= + +pytest 7.4.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0.rst new file mode 100644 index 00000000000..00f54fd8225 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0.rst @@ -0,0 +1,26 @@ +pytest-8.0.0 +======================================= + +The pytest team is proud to announce the 8.0.0 release! + +This release contains new features, improvements, bug fixes, and breaking changes, so users +are encouraged to take a look at the CHANGELOG carefully: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc1.rst new file mode 100644 index 00000000000..547c8cbc53b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc1.rst @@ -0,0 +1,82 @@ +pytest-8.0.0rc1 +======================================= + +The pytest team is proud to announce the 8.0.0rc1 release! + +This release contains new features, improvements, bug fixes, and breaking changes, so users +are encouraged to take a look at the CHANGELOG carefully: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Akhilesh Ramakrishnan +* Aleksandr Brodin +* Anthony Sottile +* Arthur Richard +* Avasam +* Benjamin Schubert +* Bruno Oliveira +* Carsten Grohmann +* Cheukting +* Chris Mahoney +* Christoph Anton Mitterer +* DetachHead +* Erik Hasse +* Florian Bruhin +* Fraser Stark +* Ha Pam +* Hugo van Kemenade +* Isaac Virshup +* Israel Fruchter +* Jens Tröger +* Jon Parise +* Kenny Y +* Lesnek +* Marc Mueller +* Michał Górny +* Mihail Milushev +* Milan Lesnek +* Miro Hrončok +* Patrick Lannigan +* Ran Benita +* Reagan Lee +* Ronny Pfannschmidt +* Sadra Barikbin +* Sean Malloy +* Sean Patrick Malloy +* Sharad Nair +* Simon Blanchard +* Sourabh Beniwal +* Stefaan Lippens +* Tanya Agarwal +* Thomas Grainger +* Tom Mortimer-Jones +* Tushar Sadhwani +* Tyler Smart +* Uday Kumar +* Warren Markham +* WarrenTheRabbit +* Zac Hatfield-Dodds +* Ziad Kermadi +* akhilramkee +* antosikv +* bowugit +* mickeypash +* neilmartin2000 +* pomponchik +* ryanpudd +* touilleWoman +* ubaumann + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc2.rst new file mode 100644 index 00000000000..1a6444c5214 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.0rc2.rst @@ -0,0 +1,32 @@ +pytest-8.0.0rc2 +======================================= + +The pytest team is proud to announce the 8.0.0rc2 prerelease! + +This is a prerelease, not intended for production use, but to test the upcoming features and improvements +in order to catch any major problems before the final version is released to the major public. + +We appreciate your help testing this out before the final release, making sure to report any +regressions to our issue tracker: + +https://github.com/pytest-dev/pytest/issues + +When doing so, please include the string ``[prerelease]`` in the title. + +You can upgrade from PyPI via: + + pip install pytest==8.0.0rc2 + +Users are encouraged to take a look at the CHANGELOG carefully: + + https://docs.pytest.org/en/release-8.0.0rc2/changelog.html + +Thanks to all the contributors to this release: + +* Ben Brown +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.1.rst new file mode 100644 index 00000000000..7d828e55bd9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.1.rst @@ -0,0 +1,21 @@ +pytest-8.0.1 +======================================= + +pytest 8.0.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Clément Robert +* Pierre Sassoulas +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.2.rst new file mode 100644 index 00000000000..c42159c57cf --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.0.2.rst @@ -0,0 +1,18 @@ +pytest-8.0.2 +======================================= + +pytest 8.0.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.0.rst new file mode 100644 index 00000000000..62cafdd78bb --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.0.rst @@ -0,0 +1,54 @@ +pytest-8.1.0 +======================================= + +The pytest team is proud to announce the 8.1.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Ben Brown +* Ben Leith +* Bruno Oliveira +* Clément Robert +* Dave Hall +* Dương Quốc Khánh +* Eero Vaher +* Eric Larson +* Fabian Sturm +* Faisal Fawad +* Florian Bruhin +* Franck Charras +* Joachim B Haga +* John Litborn +* Loïc Estève +* Marc Bresson +* Patrick Lannigan +* Pierre Sassoulas +* Ran Benita +* Reagan Lee +* Ronny Pfannschmidt +* Russell Martin +* clee2000 +* donghui +* faph +* jakkdl +* mrbean-bremen +* robotherapist +* whysage +* woutdenolf + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.1.rst new file mode 100644 index 00000000000..89b617b487d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.1.rst @@ -0,0 +1,18 @@ +pytest-8.1.1 +======================================= + +pytest 8.1.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.2.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.2.rst new file mode 100644 index 00000000000..19e41e0f7c5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.1.2.rst @@ -0,0 +1,18 @@ +pytest-8.1.2 +======================================= + +pytest 8.1.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.0.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.0.rst new file mode 100644 index 00000000000..2a63c8d8722 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.0.rst @@ -0,0 +1,43 @@ +pytest-8.2.0 +======================================= + +The pytest team is proud to announce the 8.2.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Daniel Miller +* Florian Bruhin +* HolyMagician03-UMich +* John Litborn +* Levon Saldamli +* Linghao Zhang +* Manuel López-Ibáñez +* Pierre Sassoulas +* Ran Benita +* Ronny Pfannschmidt +* Sebastian Meyer +* Shekhar verma +* Tamir Duberstein +* Tobias Stoeckmann +* dj +* jakkdl +* poulami-sau +* tserg + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.1.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.1.rst new file mode 100644 index 00000000000..4452edec110 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/release-8.2.1.rst @@ -0,0 +1,19 @@ +pytest-8.2.1 +======================================= + +pytest 8.2.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst index 8e706589876..8d47a205c71 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/announce/sprint2016.rst @@ -49,7 +49,7 @@ place on 20th, 21st, 22nd, 24th and 25th. On the 23rd we took a break day for some hot hiking in the Black Forest. Sprint activity was organised heavily around pairing, with plenty of group -discusssions to take advantage of the high bandwidth, and lightning talks +discussions to take advantage of the high bandwidth, and lightning talks as well. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst index 3a0ff126164..e04e64a76f9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/backwards-compatibility.rst @@ -22,7 +22,7 @@ b) transitional: the old and new API don't conflict We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). - A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationwarning`). + A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`). When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed. @@ -77,3 +77,21 @@ Deprecation Roadmap Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`. We track future deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub. + + +Python version support +====================== + +Released pytest versions support all Python versions that are actively maintained at the time of the release: + +============== =================== +pytest version min. Python version +============== =================== +8.0+ 3.8+ +7.1+ 3.7+ +6.2 - 7.0 3.6+ +5.0 - 6.1 3.5+ +3.3 - 4.6 2.7, 3.4+ +============== =================== + +`Status of Python Versions <https://devguide.python.org/versions/>`__. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/builtin.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/builtin.rst index c7e7863b218..458253fabbb 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/builtin.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/builtin.rst @@ -18,11 +18,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a $ pytest --fixtures -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collected 0 items - cache -- .../_pytest/cacheprovider.py:510 + cache -- .../_pytest/cacheprovider.py:549 Return a cache object that can persist state between testing sessions. cache.get(key, default) @@ -33,39 +33,89 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsys -- .../_pytest/capture.py:878 - Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. - - The captured output is made available via ``capsys.readouterr()`` method - calls, which return a ``(out, err)`` namedtuple. - ``out`` and ``err`` will be ``text`` objects. - - capsysbinary -- .../_pytest/capture.py:895 + capsysbinary -- .../_pytest/capture.py:1003 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. - capfd -- .../_pytest/capture.py:912 + Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" + + capfd -- .../_pytest/capture.py:1030 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - capfdbinary -- .../_pytest/capture.py:929 + Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" + + capfdbinary -- .../_pytest/capture.py:1057 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. - doctest_namespace [session scope] -- .../_pytest/doctest.py:731 + Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + + capsys -- .../_pytest/capture.py:976 + Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + + The captured output is made available via ``capsys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + doctest_namespace [session scope] -- .../_pytest/doctest.py:738 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1365 + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + + pytestconfig [session scope] -- .../_pytest/fixtures.py:1335 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -75,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a if pytestconfig.getoption("verbose") > 0: ... - record_property -- .../_pytest/junitxml.py:282 + record_property -- .../_pytest/junitxml.py:284 Add extra properties to the calling test. User properties become part of the test report and are available to the @@ -89,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_function(record_property): record_property("example_key", 1) - record_xml_attribute -- .../_pytest/junitxml.py:305 + record_xml_attribute -- .../_pytest/junitxml.py:307 Add extra xml attributes to the tag for the calling test. The fixture is callable with ``name, value``. The value is automatically XML-encoded. - record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343 + record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345 Record a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to writing global information regarding the entire test @@ -109,7 +159,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a record_testsuite_property("ARCH", "PPC") record_testsuite_property("STORAGE_TYPE", "CEPH") - ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + :param name: + The property name. + :param value: + The property value. Will be converted to a string. .. warning:: @@ -117,24 +170,29 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See :issue:`7767` for details. - tmpdir_factory [session scope] -- .../_pytest/legacypath.py:295 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:302 + tmpdir -- .../_pytest/legacypath.py:310 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. By default, a new base temporary directory is created each test session, and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a `legacy_path`_ object. + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - caplog -- .../_pytest/logging.py:483 + caplog -- .../_pytest/logging.py:602 Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -145,43 +203,50 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string - monkeypatch -- .../_pytest/monkeypatch.py:29 + monkeypatch -- .../_pytest/monkeypatch.py:33 A convenient fixture for monkey-patching. - The fixture provides these methods to modify objects, dictionaries or - os.environ:: + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>` + * :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>` + * :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>` + * :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>` + * :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>` + * :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>` + * :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>` + * :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>` + * :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>` All modifications will be undone after the requesting test function or - fixture has finished. The ``raising`` parameter determines if a KeyError - or AttributeError will be raised if the set/deletion operation has no target. + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. - recwarn -- .../_pytest/recwarn.py:29 + To undo modifications done by the fixture in a contained scope, + use :meth:`context() <pytest.MonkeyPatch.context>`. + + recwarn -- .../_pytest/recwarn.py:32 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. - See https://docs.python.org/library/how-to/capture-warnings.html for information + See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information on warning categories. - tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:183 + tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242 Return a :class:`pytest.TempPathFactory` instance for the test session. - tmp_path -- .../_pytest/tmpdir.py:198 + tmp_path -- .../_pytest/tmpdir.py:257 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + and old bases are removed after 3 sessions, to aid in debugging. + This behavior can be configured with :confval:`tmp_path_retention_count` and + :confval:`tmp_path_retention_policy`. + If ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a :class:`pathlib.Path` object. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/changelog.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/changelog.rst index 1acdad366da..f69b9782bbc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/changelog.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/changelog.rst @@ -28,6 +28,1385 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.2.1 (2024-05-19) +========================= + +Improvements +------------ + +- `#12334 <https://github.com/pytest-dev/pytest/issues/12334>`_: Support for Python 3.13 (beta1 at the time of writing). + + + +Bug Fixes +--------- + +- `#12120 <https://github.com/pytest-dev/pytest/issues/12120>`_: Fix `PermissionError` crashes arising from directories which are not selected on the command-line. + + +- `#12191 <https://github.com/pytest-dev/pytest/issues/12191>`_: Keyboard interrupts and system exits are now properly handled during the test collection. + + +- `#12300 <https://github.com/pytest-dev/pytest/issues/12300>`_: Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only. + + +- `#12308 <https://github.com/pytest-dev/pytest/issues/12308>`_: Fix a regression in pytest 8.2.0 where the permissions of automatically-created ``.pytest_cache`` directories became ``rwx------`` instead of the expected ``rwxr-xr-x``. + + + +Trivial/Internal Changes +------------------------ + +- `#12333 <https://github.com/pytest-dev/pytest/issues/12333>`_: pytest releases are now attested using the recent `Artifact Attestation <https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/>` support from GitHub, allowing users to verify the provenance of pytest's sdist and wheel artifacts. + + +pytest 8.2.0 (2024-04-27) +========================= + +Breaking Changes +---------------- + +- `#12089 <https://github.com/pytest-dev/pytest/pull/12089>`_: pytest now requires that :class:`unittest.TestCase` subclasses can be instantiated freely using ``MyTestCase('runTest')``. + + If the class doesn't allow this, you may see an error during collection such as ``AttributeError: 'MyTestCase' object has no attribute 'runTest'``. + + Classes which do not override ``__init__``, or do not access the test method in ``__init__`` using ``getattr`` or similar, are unaffected. + + Classes which do should take care to not crash when ``"runTest"`` is given, as is shown in `unittest.TestCases's implementation <https://github.com/python/cpython/blob/51aefc5bf907ddffaaf083ded0de773adcdf08c8/Lib/unittest/case.py#L419-L426>`_. + Alternatively, consider using :meth:`setUp <unittest.TestCase.setUp>` instead of ``__init__``. + + If you run into this issue using ``tornado.AsyncTestCase``, please see `issue 12263 <https://github.com/pytest-dev/pytest/issues/12263>`_. + + If you run into this issue using an abstract ``TestCase`` subclass, please see `issue 12275 <https://github.com/pytest-dev/pytest/issues/12275>`_. + + Historical note: the effect of this change on custom TestCase implementations was not properly considered initially, this is why it was done in a minor release. We apologize for the inconvenience. + +Deprecations +------------ + +- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it: + + - :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead. + - :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead. + - :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead. + - :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead. + - :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead. + + The replacement parameters are available since pytest 7.0.0. + The old parameters will be removed in pytest 9.0.0. + + See :ref:`legacy-path-hooks-deprecated` for more details. + + + +Features +-------- + +- `#11871 <https://github.com/pytest-dev/pytest/issues/11871>`_: Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line. + + See :ref:`Read arguments from file <args-from-file>` for details. + + + +Improvements +------------ + +- `#11523 <https://github.com/pytest-dev/pytest/issues/11523>`_: :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`. + + The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`. + + See :ref:`import-or-skip-import-error` for details. + + +- `#11728 <https://github.com/pytest-dev/pytest/issues/11728>`_: For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing. + + +- `#11777 <https://github.com/pytest-dev/pytest/issues/11777>`_: Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given. + + +- `#12112 <https://github.com/pytest-dev/pytest/issues/12112>`_: Improved namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs). + + +- `#9502 <https://github.com/pytest-dev/pytest/issues/9502>`_: Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run. + + + +Bug Fixes +--------- + +- `#12065 <https://github.com/pytest-dev/pytest/issues/12065>`_: Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``. + + Now the :attr:`request.instance <pytest.FixtureRequest.instance>` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods. + Previously it was ``None``, and all fixtures of such tests would share a single ``self``. + + +- `#12135 <https://github.com/pytest-dev/pytest/issues/12135>`_: Fixed issue where fixtures adding their finalizer multiple times to fixtures they request would cause unreliable and non-intuitive teardown ordering in some instances. + + +- `#12194 <https://github.com/pytest-dev/pytest/issues/12194>`_: Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules. + + +- `#1489 <https://github.com/pytest-dev/pytest/issues/1489>`_: Fixed some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in. + + + +Trivial/Internal Changes +------------------------ + +- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: ``pluggy>=1.5.0`` is now required. + + +- `#12167 <https://github.com/pytest-dev/pytest/issues/12167>`_: :ref:`cache <cache>`: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics. + + +pytest 8.1.2 (2024-04-26) +========================= + +Bug Fixes +--------- + +- `#12114 <https://github.com/pytest-dev/pytest/issues/12114>`_: Fixed error in :func:`pytest.approx` when used with `numpy` arrays and comparing with other types. + + +pytest 8.1.1 (2024-03-08) +========================= + +.. note:: + + This release is not a usual bug fix release -- it contains features and improvements, being a follow up + to ``8.1.0``, which has been yanked from PyPI. + +Features +-------- + +- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``. + + If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules. + + +- `#11653 <https://github.com/pytest-dev/pytest/issues/11653>`_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity. + See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details. + + + +Improvements +------------ + +- `#10865 <https://github.com/pytest-dev/pytest/issues/10865>`_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`. + Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion). + While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing. + + +- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used + as the relative directory. + + Previously this would raise an :class:`AssertionError`. + + +- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. + + This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``). + + +- `#11801 <https://github.com/pytest-dev/pytest/issues/11801>`_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes. + It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list. + + +- `#11850 <https://github.com/pytest-dev/pytest/issues/11850>`_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12. + + +- `#11962 <https://github.com/pytest-dev/pytest/issues/11962>`_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``. + + +- `#11978 <https://github.com/pytest-dev/pytest/issues/11978>`_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``. + + Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging. + + +- `#12047 <https://github.com/pytest-dev/pytest/issues/12047>`_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group. + Previously, only the first exception was reported. + + + +Bug Fixes +--------- + +- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Fixed regression where ``--importmode=importlib`` would import non-test modules more than once. + + +- `#11904 <https://github.com/pytest-dev/pytest/issues/11904>`_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``. + + This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8. + + +- `#12011 <https://github.com/pytest-dev/pytest/issues/12011>`_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed. + + +- `#12014 <https://github.com/pytest-dev/pytest/issues/12014>`_: Fix the ``stacklevel`` used when warning about marks used on fixtures. + + +- `#12039 <https://github.com/pytest-dev/pytest/issues/12039>`_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows. + + +Improved Documentation +---------------------- + +- `#11790 <https://github.com/pytest-dev/pytest/issues/11790>`_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail. + + + +Trivial/Internal Changes +------------------------ + +- `#11785 <https://github.com/pytest-dev/pytest/issues/11785>`_: Some changes were made to private functions which may affect plugins which access them: + + - ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid. + - ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid. + - The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement. + Prefer to traverse the node hierarchy itself instead. + If you really need to, copy the function from the previous pytest release. + + +- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: Delayed the deprecation of the following features to ``9.0.0``: + + * :ref:`node-ctor-fspath-deprecation`. + * :ref:`legacy-path-hooks-deprecated`. + + It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal. + + This is the reason for ``8.1.0`` being yanked. + + +pytest 8.1.0 (YANKED) +===================== + + +.. note:: + + This release has been **yanked**: it broke some plugins without the proper warning period, due to + some warnings not showing up as expected. + + See `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`__. + + +pytest 8.0.2 (2024-02-24) +========================= + +Bug Fixes +--------- + +- `#11895 <https://github.com/pytest-dev/pytest/issues/11895>`_: Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``). + + +- `#11953 <https://github.com/pytest-dev/pytest/issues/11953>`_: Fix an ``IndexError`` crash raising from ``getstatementrange_ast``. + + +- `#12021 <https://github.com/pytest-dev/pytest/issues/12021>`_: Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached. + + +pytest 8.0.1 (2024-02-16) +========================= + +Bug Fixes +--------- + +- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13. + + +- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`. + + +- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`. + + +- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating. + + +- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module. + + +- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances. + + +pytest 8.0.0 (2024-01-27) +========================= + +Bug Fixes +--------- + +- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files. + + +- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash. + + +pytest 8.0.0rc2 (2024-01-17) +============================ + + +Improvements +------------ + +- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses: + + * Report tracebacks for xfailures when ``-rx`` is set. + * Report captured output for xpasses when ``-rX`` is set. + * For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed. + +- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered. + + +Bug Fixes +--------- + +- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. + + NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist. + + +- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``. + This bug was introduced in pytest 8.0.0rc1. + + +- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code. + + This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*. + + +pytest 8.0.0rc1 (2023-12-30) +============================ + +Breaking Changes +---------------- + +Old Deprecations Are Now Errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- `#7363 <https://github.com/pytest-dev/pytest/issues/7363>`_: **PytestRemovedIn8Warning deprecation warnings are now errors by default.** + + Following our plan to remove deprecated features with as little disruption as + possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors + instead of warning messages by default. + + **The affected features will be effectively removed in pytest 8.1**, so please consult the + :ref:`deprecations` section in the docs for directions on how to update existing code. + + In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a + stopgap measure by adding this to your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestRemovedIn8Warning + + But this will stop working when pytest ``8.1`` is released. + + **If you have concerns** about the removal of a specific feature, please add a + comment to :issue:`7363`. + + +Version Compatibility +^^^^^^^^^^^^^^^^^^^^^ + +- `#11151 <https://github.com/pytest-dev/pytest/issues/11151>`_: Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <https://devguide.python.org/versions/>`__. + + +- ``pluggy>=1.3.0`` is now required. + + +Collection Changes +^^^^^^^^^^^^^^^^^^ + +In this version we've made several breaking changes to pytest's collection phase, +particularly around how filesystem directories and Python packages are collected, +fixing deficiencies and allowing for cleanups and improvements to pytest's internals. +A deprecation period for these changes was not possible. + + +- `#7777 <https://github.com/pytest-dev/pytest/issues/7777>`_: Files and directories are now collected in alphabetical order jointly, unless changed by a plugin. + Previously, files were collected before directories. + See below for an example. + + +- `#8976 <https://github.com/pytest-dev/pytest/issues/8976>`_: Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only. + Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself + (unless :confval:`python_files` was changed to allow `__init__.py` file). + + To collect the entire package, specify just the directory: `pytest pkg`. + + +- `#11137 <https://github.com/pytest-dev/pytest/issues/11137>`_: :class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File`. + + The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file. + Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module), + the module being the `__init__.py` file. + This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details). + + The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file. + + Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist, + if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files). + + +- `#7777 <https://github.com/pytest-dev/pytest/issues/7777>`_: Added a new :class:`pytest.Directory` base collection node, which all collector nodes for filesystem directories are expected to subclass. + This is analogous to the existing :class:`pytest.File` for file nodes. + + Changed :class:`pytest.Package` to be a subclass of :class:`pytest.Directory`. + A ``Package`` represents a filesystem directory which is a Python package, + i.e. contains an ``__init__.py`` file. + + :class:`pytest.Package` now only collects files in its own directory; previously it collected recursively. + Sub-directories are collected as their own collector nodes, which then collect themselves, thus creating a collection tree which mirrors the filesystem hierarchy. + + Added a new :class:`pytest.Dir` concrete collection node, a subclass of :class:`pytest.Directory`. + This node represents a filesystem directory, which is not a :class:`pytest.Package`, + that is, does not contain an ``__init__.py`` file. + Similarly to ``Package``, it only collects the files in its own directory. + + :class:`pytest.Session` now only collects the initial arguments, without recursing into directories. + This work is now done by the :func:`recursive expansion process <pytest.Collector.collect>` of directory collector nodes. + + :attr:`session.name <pytest.Session.name>` is now ``""``; previously it was the rootdir directory name. + This matches :attr:`session.nodeid <_pytest.nodes.Node.nodeid>` which has always been `""`. + + The collection tree now contains directories/packages up to the :ref:`rootdir <rootdir>`, + for initial arguments that are found within the rootdir. + For files outside the rootdir, only the immediate directory/package is collected -- + note however that collecting from outside the rootdir is discouraged. + + As an example, given the following filesystem tree:: + + myroot/ + pytest.ini + top/ + ├── aaa + │ └── test_aaa.py + ├── test_a.py + ├── test_b + │ ├── __init__.py + │ └── test_b.py + ├── test_c.py + └── zzz + ├── __init__.py + └── test_zzz.py + + the collection tree, as shown by `pytest --collect-only top/` but with the otherwise-hidden :class:`~pytest.Session` node added for clarity, + is now the following:: + + <Session> + <Dir myroot> + <Dir top> + <Dir aaa> + <Module test_aaa.py> + <Function test_it> + <Module test_a.py> + <Function test_it> + <Package test_b> + <Module test_b.py> + <Function test_it> + <Module test_c.py> + <Function test_it> + <Package zzz> + <Module test_zzz.py> + <Function test_it> + + Previously, it was:: + + <Session> + <Module top/test_a.py> + <Function test_it> + <Module top/test_c.py> + <Function test_it> + <Module top/aaa/test_aaa.py> + <Function test_it> + <Package test_b> + <Module test_b.py> + <Function test_it> + <Package zzz> + <Module test_zzz.py> + <Function test_it> + + Code/plugins which rely on a specific shape of the collection tree might need to update. + + +- `#11676 <https://github.com/pytest-dev/pytest/issues/11676>`_: The classes :class:`~_pytest.nodes.Node`, :class:`~pytest.Collector`, :class:`~pytest.Item`, :class:`~pytest.File`, :class:`~_pytest.nodes.FSCollector` are now marked abstract (see :mod:`abc`). + + We do not expect this change to affect users and plugin authors, it will only cause errors when the code is already wrong or problematic. + + +Other breaking changes +^^^^^^^^^^^^^^^^^^^^^^ + +These are breaking changes where deprecation was not possible. + + +- `#11282 <https://github.com/pytest-dev/pytest/issues/11282>`_: Sanitized the handling of the ``default`` parameter when defining configuration options. + + Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option. + + Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows: + + * If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc. + * If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``. + * If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior). + + The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases. + + +- `#11667 <https://github.com/pytest-dev/pytest/issues/11667>`_: pytest's ``setup.py`` file is removed. + If you relied on this file, e.g. to install pytest using ``setup.py install``, + please see `Why you shouldn't invoke setup.py directly <https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary>`_ for alternatives. + + +- `#9288 <https://github.com/pytest-dev/pytest/issues/9288>`_: :func:`~pytest.warns` now re-emits unmatched warnings when the context + closes -- previously it would consume all warnings, hiding those that were not + matched by the function. + + While this is a new feature, we announce it as a breaking change + because many test suites are configured to error-out on warnings, and will + therefore fail on the newly-re-emitted warnings. + + +- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or + which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change. + + + +Deprecations +------------ + +- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of ``pytest.PytestRemovedIn8Warning``, meaning this will stay a warning instead of becoming an error in the future. + + +- `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_: Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work. + + This will become an error in pytest 9.0. + + + +Features and Improvements +------------------------- + +Improved Diffs +^^^^^^^^^^^^^^ + +These changes improve the diffs that pytest prints when an assertion fails. +Note that syntax highlighting requires the ``pygments`` package. + + +- `#11520 <https://github.com/pytest-dev/pytest/issues/11520>`_: The very verbose (``-vv``) diff output is now colored as a diff instead of a big chunk of red. + + Python code in error reports is now syntax-highlighted as Python. + + The sections in the error reports are now better separated. + + +- `#1531 <https://github.com/pytest-dev/pytest/issues/1531>`_: The very verbose diff (``-vv``) for every standard library container type is improved. The indentation is now consistent and the markers are on their own separate lines, which should reduce the diffs shown to users. + + Previously, the standard Python pretty printer was used to generate the output, which puts opening and closing + markers on the same line as the first/last entry, in addition to not having consistent indentation. + + +- `#10617 <https://github.com/pytest-dev/pytest/issues/10617>`_: Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with + the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``. + + +Separate Control For Assertion Verbosity +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- `#11387 <https://github.com/pytest-dev/pytest/issues/11387>`_: Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity. + + If you've ever wished that pytest always show you full diffs, but without making everything else verbose, this is for you. + + See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details. + + For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type. + + +Additional Support For Exception Groups and ``__notes__`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These changes improve pytest's support for exception groups. + + +- `#10441 <https://github.com/pytest-dev/pytest/issues/10441>`_: Added :func:`ExceptionInfo.group_contains() <pytest.ExceptionInfo.group_contains>`, an assertion helper that tests if an :class:`ExceptionGroup` contains a matching exception. + + See :ref:`assert-matching-exception-groups` for an example. + + +- `#11227 <https://github.com/pytest-dev/pytest/issues/11227>`_: Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``. + + +Custom Directory collectors +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- `#7777 <https://github.com/pytest-dev/pytest/issues/7777>`_: Added a new hook :hook:`pytest_collect_directory`, + which is called by filesystem-traversing collector nodes, + such as :class:`pytest.Session`, :class:`pytest.Dir` and :class:`pytest.Package`, + to create a collector node for a sub-directory. + It is expected to return a subclass of :class:`pytest.Directory`. + This hook allows plugins to :ref:`customize the collection of directories <custom directory collectors>`. + + +"New-style" Hook Wrappers +^^^^^^^^^^^^^^^^^^^^^^^^^ + +- `#11122 <https://github.com/pytest-dev/pytest/issues/11122>`_: pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0. + See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details. + + Plugins which want to use new-style wrappers can do so if they require ``pytest>=8``. + + +Other Improvements +^^^^^^^^^^^^^^^^^^ + +- `#11216 <https://github.com/pytest-dev/pytest/issues/11216>`_: If a test is skipped from inside an :ref:`xunit setup fixture <classic xunit>`, the test summary now shows the test location instead of the fixture location. + + +- `#11314 <https://github.com/pytest-dev/pytest/issues/11314>`_: Logging to a file using the ``--log-file`` option will use ``--log-level``, ``--log-format`` and ``--log-date-format`` as fallback + if ``--log-file-level``, ``--log-file-format`` and ``--log-file-date-format`` are not provided respectively. + + +- `#11610 <https://github.com/pytest-dev/pytest/issues/11610>`_: Added the :func:`LogCaptureFixture.filtering() <pytest.LogCaptureFixture.filtering>` context manager which + adds a given :class:`logging.Filter` object to the :fixture:`caplog` fixture. + + +- `#11447 <https://github.com/pytest-dev/pytest/issues/11447>`_: :func:`pytest.deprecated_call` now also considers warnings of type :class:`FutureWarning`. + + +- `#11600 <https://github.com/pytest-dev/pytest/issues/11600>`_: Improved the documentation and type signature for :func:`pytest.mark.xfail <pytest.mark.xfail>`'s ``condition`` param to use ``False`` as the default value. + + +- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_: :class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes. + + +- `#11353 <https://github.com/pytest-dev/pytest/issues/11353>`_: Added typing to :class:`~pytest.PytestPluginManager`. + + +Bug Fixes +--------- + +- `#10701 <https://github.com/pytest-dev/pytest/issues/10701>`_: :meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list, + rather than the first warning which is an instance of the requested type. + + +- `#11255 <https://github.com/pytest-dev/pytest/issues/11255>`_: Fixed crash on `parametrize(..., scope="package")` without a package present. + + +- `#11277 <https://github.com/pytest-dev/pytest/issues/11277>`_: Fixed a bug that when there are multiple fixtures for an indirect parameter, + the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope. + + +- `#11456 <https://github.com/pytest-dev/pytest/issues/11456>`_: Parametrized tests now *really do* ensure that the ids given to each input are unique - for + example, ``a, a, a0`` now results in ``a1, a2, a0`` instead of the previous (buggy) ``a0, a1, a0``. + This necessarily means changing nodeids where these were previously colliding, and for + readability adds an underscore when non-unique ids end in a number. + + +- `#11563 <https://github.com/pytest-dev/pytest/issues/11563>`_: Fixed a crash when using an empty string for the same parametrized value more than once. + + +- `#11712 <https://github.com/pytest-dev/pytest/issues/11712>`_: Fixed handling ``NO_COLOR`` and ``FORCE_COLOR`` to ignore an empty value. + + +- `#9036 <https://github.com/pytest-dev/pytest/issues/9036>`_: ``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block. + + + +Improved Documentation +---------------------- + +- `#11011 <https://github.com/pytest-dev/pytest/issues/11011>`_: Added a warning about modifying the root logger during tests when using ``caplog``. + + +- `#11065 <https://github.com/pytest-dev/pytest/issues/11065>`_: Use ``pytestconfig`` instead of ``request.config`` in cache example to be consistent with the API documentation. + + +Trivial/Internal Changes +------------------------ + +- `#11208 <https://github.com/pytest-dev/pytest/issues/11208>`_: The (internal) ``FixtureDef.cached_result`` type has changed. + Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet. + + +- `#11218 <https://github.com/pytest-dev/pytest/issues/11218>`_: (This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.) + + :class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly. + A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions, + as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions. + + +- `#11315 <https://github.com/pytest-dev/pytest/issues/11315>`_: The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory. + If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored. + Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead. + + +- `#11333 <https://github.com/pytest-dev/pytest/issues/11333>`_: Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``. + The previous spelling ``INCOVATION_DIR`` remains as an alias. + + +- `#11638 <https://github.com/pytest-dev/pytest/issues/11638>`_: Fixed the selftests to pass correctly if ``FORCE_COLOR``, ``NO_COLOR`` or ``PY_COLORS`` is set in the calling environment. + +pytest 7.4.4 (2023-12-31) +========================= + +Bug Fixes +--------- + +- `#11140 <https://github.com/pytest-dev/pytest/issues/11140>`_: Fix non-string constants at the top of file being detected as docstrings on Python>=3.8. + + +- `#11572 <https://github.com/pytest-dev/pytest/issues/11572>`_: Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down. + + +- `#11710 <https://github.com/pytest-dev/pytest/issues/11710>`_: Fixed tracebacks from collection errors not getting pruned. + + +- `#7966 <https://github.com/pytest-dev/pytest/issues/7966>`_: Removed unhelpful error message from assertion rewrite mechanism when exceptions are raised in ``__iter__`` methods. Now they are treated un-iterable instead. + + + +Improved Documentation +---------------------- + +- `#11091 <https://github.com/pytest-dev/pytest/issues/11091>`_: Updated documentation to refer to hyphenated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``. + + +pytest 7.4.3 (2023-10-24) +========================= + +Bug Fixes +--------- + +- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression. + + +- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases. + + +- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down. + + +pytest 7.4.2 (2023-09-07) +========================= + +Bug Fixes +--------- + +- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects. + + +- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases. + + +- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown. + + +- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files. + + + +Improved Documentation +---------------------- + +- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing. + + +pytest 7.4.1 (2023-09-02) +========================= + +Bug Fixes +--------- + +- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the + child modules as attributes of the parent modules. + + +- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries. + + +- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules + to be imported more than once, causing problems with modules that have import side effects. + + +pytest 7.4.0 (2023-06-23) +========================= + +Features +-------- + +- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception. + This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses. + + + +Improvements +------------ + +- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook. + + +- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown: + + "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.". + + Previously, the last frame of the traceback was shown, even though it was hidden. + + +- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output. + + Added ``TerminalReporter.wrap_write()`` as a helper for that. + + +- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``. + + +- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages. + + +- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory. + + +- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`. + Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem. + If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`. + + +- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it. + + If after updating to this version you see that your `norecursedirs` setting is not being respected, + it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation. + Most likely, your hook returns `False` for paths it does not want to ignore, + which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path. + The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore. + + +- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>` + will temporarily enable the requested ``level`` if ``level`` was disabled globally via + ``logging.disable(LEVEL)``. + + + +Bug Fixes +--------- + +- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``. + + +- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`. + + +- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests, + even when it was not utilized (e.g. when explicit paths were given on the command line). + Now the ``testpaths`` are only considered when they are in use. + + +- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message). + + +- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``. + + + +Improved Documentation +---------------------- + +- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`. + + + +Trivial/Internal Changes +------------------------ + +- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file. + + +pytest 7.3.2 (2023-06-10) +========================= + +Bug Fixes +--------- + +- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems. + + +- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing). + + +- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``. + + +- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments. + + +- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call. + + +- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files). + + +pytest 7.3.1 (2023-04-14) +========================= + +Improvements +------------ + +- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests. + + +- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`. + + + +Bug Fixes +--------- + +- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option. + + +- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden. + This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0. + + +pytest 7.3.0 (2023-04-08) +========================= + +Features +-------- + +- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods. + + +- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact. + + +- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers. + + +- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept. + + + +Improvements +------------ + +- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last. + + +- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of + ``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration + file. + + +- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects. + + +- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line. + + +- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`. + + +- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than + the hidden text. The line number shown has also been fixed. + + + +Bug Fixes +--------- + +- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator. + + +- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`. + + +- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions. + NOTE: This change was reverted in version 7.3.1. + + + +Improved Documentation +---------------------- + +- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully. + + + +Trivial/Internal Changes +------------------------ + +- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While + we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support, + it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects. + With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead. + + Nice diffs for ``attrs`` classes are still supported though. + + +pytest 7.2.2 (2023-03-03) +========================= + +Bug Fixes +--------- + +- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`. + + +- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time. + + +- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage. + + +- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time. + + +- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow + :code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'. + + + +Improved Documentation +---------------------- + +- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation. + + +- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch. + + +- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit + about not collecting tests and not executing the rest of the module. + + +pytest 7.2.1 (2023-01-13) +========================= + +Bug Fixes +--------- + +- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12. + + +- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location. + + +- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows. + + +- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel. + + +- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache. + + +pytest 7.2.0 (2022-10-23) +========================= + +Deprecations +------------ + +- `#10012 <https://github.com/pytest-dev/pytest/issues/10012>`_: Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8. + + +- `#10396 <https://github.com/pytest-dev/pytest/issues/10396>`_: pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency. + + +- `#4562 <https://github.com/pytest-dev/pytest/issues/4562>`_: Deprecate configuring hook specs/impls using attributes/marks. + + Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`. + For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`. + + +- `#9886 <https://github.com/pytest-dev/pytest/issues/9886>`_: The functionality for running tests written for ``nose`` has been officially deprecated. + + This includes: + + * Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support. + * Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator. + + For more details, consult the :ref:`deprecation docs <nose-deprecation>`. + + .. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup + +- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future. + + +Features +-------- + +- `#9897 <https://github.com/pytest-dev/pytest/issues/9897>`_: Added shell-style wildcard support to ``testpaths``. + + + +Improvements +------------ + +- `#10218 <https://github.com/pytest-dev/pytest/issues/10218>`_: ``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names, + instead of just ``list[str]`` and ``tuple[str, ...]``. + + (Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a + comma-delimited name list, as before). + + +- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``. + + +- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead. + + +- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and + enhance match comparison for :py:func:`pytest.ExceptionInfo.match` as returned by :py:func:`pytest.raises`. + + +- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing + error. We now raise immediately with a more helpful message. + + +- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML. + + `tomli` is no longer a dependency on Python 3.11. + + +- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``. + + +- `#9823 <https://github.com/pytest-dev/pytest/issues/9823>`_: Improved error message that is shown when no collector is found for a given file. + + +- `#9873 <https://github.com/pytest-dev/pytest/issues/9873>`_: Some coloring has been added to the short test summary. + + +- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options. + + +- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment. + + +- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``. + + + +Bug Fixes +--------- + +- `#10150 <https://github.com/pytest-dev/pytest/issues/10150>`_: :data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled. + + +- `#10382 <https://github.com/pytest-dev/pytest/issues/10382>`_: Do not break into pdb when ``raise unittest.SkipTest()`` appears top-level in a file. + + +- `#7792 <https://github.com/pytest-dev/pytest/issues/7792>`_: Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply. + + When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse. + + When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` instead. + + +- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``. + + +- `#9877 <https://github.com/pytest-dev/pytest/issues/9877>`_: Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``. + + + +Improved Documentation +---------------------- + +- `#10344 <https://github.com/pytest-dev/pytest/issues/10344>`_: Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``. + + +- `#9248 <https://github.com/pytest-dev/pytest/issues/9248>`_: The documentation is now built using Sphinx 5.x (up from 3.x previously). + + +- `#9291 <https://github.com/pytest-dev/pytest/issues/9291>`_: Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`. + + + +Trivial/Internal Changes +------------------------ + +- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for + type check and runtime purposes. Made `_pytest.doctest` use internal APIs + to avoid circular imports. + + +- `#9906 <https://github.com/pytest-dev/pytest/issues/9906>`_: Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers. + + +- `#9910 <https://github.com/pytest-dev/pytest/issues/9910>`_: Fix default encoding warning (``EncodingWarning``) in ``cacheprovider`` + + +- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been + torn down. + Add an additional sentence to the docstring explaining when it's not a good + idea to call ``getfixturevalue``. + + +pytest 7.1.3 (2022-08-31) +========================= + +Bug Fixes +--------- + +- `#10060 <https://github.com/pytest-dev/pytest/issues/10060>`_: When running with ``--pdb``, ``TestCase.tearDown`` is no longer called for tests when the *class* has been skipped via ``unittest.skip`` or ``pytest.mark.skip``. + + +- `#10190 <https://github.com/pytest-dev/pytest/issues/10190>`_: Invalid XML characters in setup or teardown error messages are now properly escaped for JUnit XML reports. + + +- `#10230 <https://github.com/pytest-dev/pytest/issues/10230>`_: Ignore ``.py`` files created by ``pyproject.toml``-based editable builds introduced in `pip 21.3 <https://pip.pypa.io/en/stable/news/#v21-3>`__. + + +- `#3396 <https://github.com/pytest-dev/pytest/issues/3396>`_: Doctests now respect the ``--import-mode`` flag. + + +- `#9514 <https://github.com/pytest-dev/pytest/issues/9514>`_: Type-annotate ``FixtureRequest.param`` as ``Any`` as a stop gap measure until :issue:`8073` is fixed. + + +- `#9791 <https://github.com/pytest-dev/pytest/issues/9791>`_: Fixed a path handling code in ``rewrite.py`` that seems to work fine, but was incorrect and fails in some systems. + + +- `#9917 <https://github.com/pytest-dev/pytest/issues/9917>`_: Fixed string representation for :func:`pytest.approx` when used to compare tuples. + + + +Improved Documentation +---------------------- + +- `#9937 <https://github.com/pytest-dev/pytest/issues/9937>`_: Explicit note that :fixture:`tmpdir` fixture is discouraged in favour of :fixture:`tmp_path`. + + + +Trivial/Internal Changes +------------------------ + +- `#10114 <https://github.com/pytest-dev/pytest/issues/10114>`_: Replace `atomicwrites <https://github.com/untitaker/python-atomicwrites>`__ dependency on windows with `os.replace`. + + +pytest 7.1.2 (2022-04-23) +========================= + +Bug Fixes +--------- + +- `#9726 <https://github.com/pytest-dev/pytest/issues/9726>`_: An unnecessary ``numpy`` import inside :func:`pytest.approx` was removed. + + +- `#9820 <https://github.com/pytest-dev/pytest/issues/9820>`_: Fix comparison of ``dataclasses`` with ``InitVar``. + + +- `#9869 <https://github.com/pytest-dev/pytest/issues/9869>`_: Increase ``stacklevel`` for the ``NODE_CTOR_FSPATH_ARG`` deprecation to point to the + user's code, not pytest. + + +- `#9871 <https://github.com/pytest-dev/pytest/issues/9871>`_: Fix a bizarre (and fortunately rare) bug where the `temp_path` fixture could raise + an internal error while attempting to get the current user's username. + + +pytest 7.1.1 (2022-03-17) +========================= + +Bug Fixes +--------- + +- `#9767 <https://github.com/pytest-dev/pytest/issues/9767>`_: Fixed a regression in pytest 7.1.0 where some conftest.py files outside of the source tree (e.g. in the `site-packages` directory) were not picked up. + + +pytest 7.1.0 (2022-03-13) +========================= + +Breaking Changes +---------------- + +- `#8838 <https://github.com/pytest-dev/pytest/issues/8838>`_: As per our policy, the following features have been deprecated in the 6.X series and are now + removed: + + * ``pytest._fillfuncargs`` function. + + * ``pytest_warning_captured`` hook - use ``pytest_warning_recorded`` instead. + + * ``-k -foobar`` syntax - use ``-k 'not foobar'`` instead. + + * ``-k foobar:`` syntax. + + * ``pytest.collect`` module - import from ``pytest`` directly. + + For more information consult + `Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`__ in the docs. + + +- `#9437 <https://github.com/pytest-dev/pytest/issues/9437>`_: Dropped support for Python 3.6, which reached `end-of-life <https://devguide.python.org/#status-of-python-branches>`__ at 2021-12-23. + + + +Improvements +------------ + +- `#5192 <https://github.com/pytest-dev/pytest/issues/5192>`_: Fixed test output for some data types where ``-v`` would show less information. + + Also, when showing diffs for sequences, ``-q`` would produce full diffs instead of the expected diff. + + +- `#9362 <https://github.com/pytest-dev/pytest/issues/9362>`_: pytest now avoids specialized assert formatting when it is detected that the default ``__eq__`` is overridden in ``attrs`` or ``dataclasses``. + + +- `#9536 <https://github.com/pytest-dev/pytest/issues/9536>`_: When ``-vv`` is given on command line, show skipping and xfail reasons in full instead of truncating them to fit the terminal width. + + +- `#9644 <https://github.com/pytest-dev/pytest/issues/9644>`_: More information about the location of resources that led Python to raise :class:`ResourceWarning` can now + be obtained by enabling :mod:`tracemalloc`. + + See :ref:`resource-warnings` for more information. + + +- `#9678 <https://github.com/pytest-dev/pytest/issues/9678>`_: More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``. + Previously only `str`, `float`, `int` and `bool` were accepted; + now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted. + + +- `#9692 <https://github.com/pytest-dev/pytest/issues/9692>`_: :func:`pytest.approx` now raises a :class:`TypeError` when given an unordered sequence (such as :class:`set`). + + Note that this implies that custom classes which only implement ``__iter__`` and ``__len__`` are no longer supported as they don't guarantee order. + + + +Bug Fixes +--------- + +- `#8242 <https://github.com/pytest-dev/pytest/issues/8242>`_: The deprecation of raising :class:`unittest.SkipTest` to skip collection of + tests during the pytest collection phase is reverted - this is now a supported + feature again. + + +- `#9493 <https://github.com/pytest-dev/pytest/issues/9493>`_: Symbolic link components are no longer resolved in conftest paths. + This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice. + For example, given + + tests/real/conftest.py + tests/real/test_it.py + tests/link -> tests/real + + running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``. + This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details). + + +- `#9626 <https://github.com/pytest-dev/pytest/issues/9626>`_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules. + + If there were errors or skipped modules on collection, pytest would mistakenly subtract those from the selected count. + + +- `#9645 <https://github.com/pytest-dev/pytest/issues/9645>`_: Fixed regression where ``--import-mode=importlib`` used together with :envvar:`PYTHONPATH` or :confval:`pythonpath` would cause import errors in test suites. + + +- `#9708 <https://github.com/pytest-dev/pytest/issues/9708>`_: :fixture:`pytester` now requests a :fixture:`monkeypatch` fixture instead of creating one internally. This solves some issues with tests that involve pytest environment variables. + + +- `#9730 <https://github.com/pytest-dev/pytest/issues/9730>`_: Malformed ``pyproject.toml`` files now produce a clearer error message. + + pytest 7.0.1 (2022-02-11) ========================= @@ -64,7 +1443,7 @@ Deprecations ``__init__`` method, they should take ``**kwargs``. See :ref:`uncooperative-constructors-deprecated` for details. - Note that a deprection warning is only emitted when there is a conflict in the + Note that a deprecation warning is only emitted when there is a conflict in the arguments pytest expected to pass. This deprecation was already part of pytest 7.0.0rc1 but wasn't documented. @@ -76,7 +1455,7 @@ Bug Fixes - `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above. -- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``). +- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure `pytest.Config.inifile` is available during the :hook:`pytest_cmdline_main` hook (regression during ``7.0.0rc1``). @@ -106,7 +1485,7 @@ Breaking Changes - `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`. Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation. - Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted. + Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffected. Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`. Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead. @@ -211,6 +1590,8 @@ Deprecations :class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` / :func:`unittest.skip` in unittest test cases is fully supported. + .. note:: This deprecation has been reverted in pytest 7.1.0. + - `#8315 <https://github.com/pytest-dev/pytest/issues/8315>`_: Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now scheduled for removal in pytest 8 (deprecated since pytest 2.4.0): @@ -219,13 +1600,13 @@ Deprecations - ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. -- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning. +- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning. It was never sanely supported and triggers hard to debug errors. See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details. -- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead. +- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: ``pytest_cmdline_preparse`` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead. See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details. @@ -261,7 +1642,7 @@ Features - `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used. -- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing :meth:`cache.makedir() <pytest.Cache.makedir>`, +- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing ``cache.makedir()``, but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``. Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`, @@ -287,7 +1668,7 @@ Features - ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`. - ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`. - ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`. - - ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others. + - ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.LineMatcher>` type used in :class:`~pytest.RunResult` and others. - ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks. - ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks. @@ -320,7 +1701,7 @@ Features - `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet - due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`, we expect to deprecate it in a future release. + due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`, we expect to deprecate it in a future release. .. note:: The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the @@ -352,7 +1733,7 @@ Features See :ref:`plugin-stash` for details. -- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a +- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a ``warnings`` argument to assert the total number of warnings captured. @@ -364,7 +1745,7 @@ Features used. -- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a +- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a ``deselected`` argument to assert the total number of deselected tests. @@ -377,7 +1758,7 @@ Improvements - `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`, a subclass of :class:`~pytest.PytestDeprecationWarning`, - instead of :class:`PytestDeprecationWarning` directly. + instead of :class:`~pytest.PytestDeprecationWarning` directly. See :ref:`backwards-compatibility` for more details. @@ -416,7 +1797,7 @@ Improvements - `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log. - By using :func:`add_color_level <_pytest.logging.add_color_level>` from a ``pytest_configure`` hook, colors can be added:: + By using ``add_color_level`` from a :hook:`pytest_configure` hook, colors can be added:: logging_plugin = config.pluginmanager.get_plugin('logging-plugin') logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan') @@ -481,7 +1862,7 @@ Bug Fixes - `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when ``setuptools`` is not installed. - It now only calls :func:`pkg_resources.fixup_namespace_packages` if + It now only calls ``pkg_resources.fixup_namespace_packages`` if ``pkg_resources`` was previously imported, because it is not needed otherwise. @@ -612,7 +1993,7 @@ Bug Fixes the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with private permissions. - pytest used to silently use a pre-existing ``/tmp/pytest-of-<username>`` directory, + pytest used to silently use a preexisting ``/tmp/pytest-of-<username>`` directory, even if owned by another user. This means another user could pre-create such a directory and gain control of another user's temporary directory. Now such a condition results in an error. @@ -708,7 +2089,7 @@ Features This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. - Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface. + Internally, the old ``pytest.Testdir`` is now a thin wrapper around :class:`~pytest.Pytester`, preserving the old interface. - :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary. @@ -746,7 +2127,7 @@ Features Improvements ------------ -- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. +- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. - :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". @@ -810,7 +2191,7 @@ Bug Fixes - :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. -- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved. +- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <pytest.Pytester.spawn>` when the :mod:`readline` module is involved. - :issue:`7951`: Fixed handling of recursive symlinks when collecting tests. @@ -927,7 +2308,7 @@ Deprecations if you use this and want a replacement. -- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor +- :issue:`7255`: The ``pytest_warning_captured`` hook is deprecated in favor of :hook:`pytest_warning_recorded`, and will be removed in a future version. @@ -955,8 +2336,8 @@ Improvements - :issue:`7572`: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace. -- :issue:`7685`: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`. - These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes, +- :issue:`7685`: Added two new attributes :attr:`rootpath <pytest.Config.rootpath>` and :attr:`inipath <pytest.Config.inipath>` to :class:`~pytest.Config`. + These attributes are :class:`pathlib.Path` versions of the existing ``rootdir`` and ``inifile`` attributes, and should be preferred over them when possible. @@ -1027,7 +2408,7 @@ Trivial/Internal Changes - :issue:`7587`: The dependency on the ``more-itertools`` package has been removed. -- :issue:`7631`: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, +- :issue:`7631`: The result type of :meth:`capfd.readouterr() <pytest.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, but should behave like one in all respects. This was done for technical reasons. @@ -1339,7 +2720,7 @@ Features also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``). - ``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't + ``--import-mode=importlib`` uses more fine-grained import mechanisms from ``importlib`` which don't require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks of the previous mode. @@ -1356,7 +2737,7 @@ Improvements ------------ - :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that - is printed to stderr when the output of ``pytest`` is piped and and the pipe is + is printed to stderr when the output of ``pytest`` is piped and the pipe is closed by the piped-to program (common examples are ``less`` and ``head``). @@ -1405,10 +2786,10 @@ Improvements - :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`. -- :issue:`7133`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file. +- :issue:`7133`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file. -- :issue:`7159`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect +- :issue:`7159`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <pytest.LogCaptureFixture.at_level>` no longer affect the level of logs that are shown in the *Captured log report* report section. @@ -1503,7 +2884,7 @@ Bug Fixes parameter when Python is called with the ``-bb`` flag. -- :issue:`7143`: Fix :meth:`pytest.File.from_parent` so it forwards extra keyword arguments to the constructor. +- :issue:`7143`: Fix :meth:`pytest.File.from_parent <_pytest.nodes.Node.from_parent>` so it forwards extra keyword arguments to the constructor. - :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. @@ -1658,7 +3039,7 @@ Breaking Changes This hook has been marked as deprecated and not been even called by pytest for over 10 years now. -- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result. +- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that something expected is missing in the result and "+" means that there are unexpected extras in the result. - :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when @@ -1754,7 +3135,7 @@ Improvements - :issue:`6384`: Make `--showlocals` work also with `--tb=short`. -- :issue:`6653`: Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`. +- :issue:`6653`: Add support for matching lines consecutively with :class:`~pytest.LineMatcher`'s :func:`~pytest.LineMatcher.fnmatch_lines` and :func:`~pytest.LineMatcher.re_match_lines`. - :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed. @@ -1822,7 +3203,7 @@ Bug Fixes - :issue:`6597`: Fix node ids which contain a parametrized empty-string variable. -- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc. +- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's ``testdir.runpytest`` etc. - :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger. @@ -1888,7 +3269,7 @@ Bug Fixes ``multiprocessing`` module. -- :issue:`6436`: :class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and +- :issue:`6436`: :class:`~pytest.FixtureDef` objects now properly register their finalizers with autouse and parameterized fixtures that execute before them in the fixture stack so they are torn down at the right times, and in the right order. @@ -1944,7 +3325,7 @@ Improvements Bug Fixes --------- -- :issue:`5914`: pytester: fix :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` when used after positive matching. +- :issue:`5914`: pytester: fix :py:func:`~pytest.LineMatcher.no_fnmatch_line` when used after positive matching. - :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`. @@ -2008,8 +3389,8 @@ Features rather than implicitly. -- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and - :py:func:`~_pytest.pytester.LineMatcher.no_re_match_line`. +- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~pytest.LineMatcher.no_fnmatch_line` and + :py:func:`~pytest.LineMatcher.no_re_match_line`. The functions are used to ensure the captured text *does not* match the given pattern. @@ -2495,7 +3876,8 @@ Important This release is a Python3.5+ only release. -For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`. +For more details, see our `Python 2.7 and 3.4 support plan +<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_. Removals -------- @@ -2719,7 +4101,11 @@ Features - :issue:`6870`: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. - Remark: while this is technically a new feature and according to our :ref:`policy <what goes into 4.6.x releases>` it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix. + Remark: while this is technically a new feature and according to our + `policy <https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_ + it should not have been backported, we have opened an exception in this + particular case because it fixes a serious interaction with ``pytest-xdist``, + so it can also be considered a bugfix. Trivial/Internal Changes ------------------------ @@ -2891,7 +4277,8 @@ Important The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**. -For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`. +For more details, see our `Python 2.7 and 3.4 support plan +<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_. Features @@ -3257,7 +4644,7 @@ Bug Fixes Improved Documentation ---------------------- -- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations +- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability limitations @@ -3598,7 +4985,7 @@ Removals See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code. -- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check. +- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check. Use ``Node.get_closest_marker(name)`` as a replacement. @@ -5115,7 +6502,7 @@ Features Bug Fixes --------- -- Fix hanging pexpect test on MacOS by using flush() instead of wait(). +- Fix hanging pexpect test on macOS by using flush() instead of wait(). (:issue:`2022`) - Fix restoring Python state after in-process pytest runs with the @@ -5163,7 +6550,7 @@ Trivial/Internal Changes ------------------------ - Show a simple and easy error when keyword expressions trigger a syntax error - (for example, ``"-k foo and import"`` will show an error that you can not use + (for example, ``"-k foo and import"`` will show an error that you cannot use the ``import`` keyword in expressions). (:issue:`2953`) - Change parametrized automatic test id generation to use the ``__name__`` @@ -5855,7 +7242,7 @@ Changes * fix :issue:`2013`: turn RecordedWarning into ``namedtuple``, to give it a comprehensible repr while preventing unwarranted modification. -* fix :issue:`2208`: ensure an iteration limit for _pytest.compat.get_real_func. +* fix :issue:`2208`: ensure an iteration limit for ``_pytest.compat.get_real_func``. Thanks :user:`RonnyPfannschmidt` for the report and PR. * Hooks are now verified after collection is complete, rather than right after loading installed plugins. This @@ -6059,7 +7446,7 @@ Bug Fixes Thanks :user:`adborden` for the report and :user:`nicoddemus` for the PR. * Clean up unittest TestCase objects after tests are complete (:issue:`1649`). - Thanks :user:`d_b_w` for the report and PR. + Thanks :user:`d-b-w` for the report and PR. 3.0.3 (2016-09-28) @@ -6074,7 +7461,7 @@ Bug Fixes Thanks :user:`nicoddemus` for the PR. * Fix pkg_resources import error in Jython projects (:issue:`1853`). - Thanks :user:`raquel-ucl` for the PR. + Thanks :user:`raquelalegre` for the PR. * Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception in Python 3 (:issue:`1944`). @@ -6939,7 +8326,7 @@ time or change existing behaviors in order to make them less surprising/more use one will also have a "reprec" attribute with the recorded events/reports. - fix monkeypatch.setattr("x.y", raising=False) to actually not raise - if "y" is not a pre-existing attribute. Thanks Florian Bruhin. + if "y" is not a preexisting attribute. Thanks Florian Bruhin. - fix issue741: make running output from testdir.run copy/pasteable Thanks Bruno Oliveira. @@ -6995,7 +8382,7 @@ time or change existing behaviors in order to make them less surprising/more use - fix issue854: autouse yield_fixtures defined as class members of unittest.TestCase subclasses now work as expected. - Thannks xmo-odoo for the report and Bruno Oliveira for the PR. + Thanks xmo-odoo for the report and Bruno Oliveira for the PR. - fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the fixtures declared on the first one. @@ -7099,7 +8486,7 @@ time or change existing behaviors in order to make them less surprising/more use github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. -- fix issue650: new option ``--docttest-ignore-import-errors`` which +- fix issue650: new option ``--doctest-ignore-import-errors`` which will turn import errors in doctests into skips. Thanks Charles Cloud for the complete PR. @@ -7287,7 +8674,7 @@ time or change existing behaviors in order to make them less surprising/more use - cleanup setup.py a bit and specify supported versions. Thanks Jurko Gospodnetic for the PR. -- change XPASS colour to yellow rather then red when tests are run +- change XPASS colour to yellow rather than red when tests are run with -v. - fix issue473: work around mock putting an unbound method into a class @@ -7460,7 +8847,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks Ralph Schmitt for the precise failure example. - fix issue244 by implementing special index for parameters to only use - indices for paramentrized test ids + indices for parametrized test ids - fix issue287 by running all finalizers but saving the exception from the first failing finalizer and re-raising it so teardown will @@ -7468,7 +8855,7 @@ time or change existing behaviors in order to make them less surprising/more use it might be the cause for other finalizers to fail. - fix ordering when mock.patch or other standard decorator-wrappings - are used with test methods. This fixues issue346 and should + are used with test methods. This fixes issue346 and should help with random "xdist" collection failures. Thanks to Ronny Pfannschmidt and Donald Stufft for helping to isolate it. @@ -7725,7 +9112,7 @@ Bug fixes: partially failed (finalizers would not always be called before) - fix issue320 - fix class scope for fixtures when mixed with - module-level functions. Thanks Anatloy Bubenkoff. + module-level functions. Thanks Anatoly Bubenkoff. - you can specify "-q" or "-qq" to get different levels of "quieter" reporting (thanks Katarzyna Jachim) @@ -8147,7 +9534,7 @@ Bug fixes: unexpected exceptions - fix issue47: timing output in junitxml for test cases is now correct - fix issue48: typo in MarkInfo repr leading to exception -- fix issue49: avoid confusing error when initizaliation partially fails +- fix issue49: avoid confusing error when initialization partially fails - fix issue44: env/username expansion for junitxml file path - show releaselevel information in test runs for pypy - reworked doc pages for better navigation and PDF generation @@ -8272,7 +9659,7 @@ Bug fixes: collection-before-running semantics were not setup as with pytest 1.3.4. Note, however, that the recommended and much cleaner way to do test - parametraization remains the "pytest_generate_tests" + parameterization remains the "pytest_generate_tests" mechanism, see the docs. 2.0.0 (2010-11-25) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/conf.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/conf.py index b316163532a..32ecaa17435 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/conf.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/conf.py @@ -15,16 +15,15 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -import ast import os import shutil import sys from textwrap import dedent -from typing import List from typing import TYPE_CHECKING from _pytest import __version__ as version + if TYPE_CHECKING: import sphinx.application @@ -38,6 +37,7 @@ release = ".".join(version.split(".")[:2]) autodoc_member_order = "bysource" autodoc_typehints = "description" +autodoc_typehints_description_target = "documented" todo_include_todos = 1 latex_engine = "lualatex" @@ -162,14 +162,58 @@ linkcheck_workers = 5 _repo = "https://github.com/pytest-dev/pytest" extlinks = { - "bpo": ("https://bugs.python.org/issue%s", "bpo-"), - "pypi": ("https://pypi.org/project/%s/", ""), - "issue": (f"{_repo}/issues/%s", "issue #"), - "pull": (f"{_repo}/pull/%s", "pull request #"), - "user": ("https://github.com/%s", "@"), + "bpo": ("https://bugs.python.org/issue%s", "bpo-%s"), + "pypi": ("https://pypi.org/project/%s/", "%s"), + "issue": (f"{_repo}/issues/%s", "issue #%s"), + "pull": (f"{_repo}/pull/%s", "pull request #%s"), + "user": ("https://github.com/%s", "@%s"), } +nitpicky = True +nitpick_ignore = [ + # TODO (fix in pluggy?) + ("py:class", "HookCaller"), + ("py:class", "HookspecMarker"), + ("py:exc", "PluginValidationError"), + # Might want to expose/TODO (https://github.com/pytest-dev/pytest/issues/7469) + ("py:class", "ExceptionRepr"), + ("py:class", "Exit"), + ("py:class", "SubRequest"), + ("py:class", "SubRequest"), + ("py:class", "TerminalReporter"), + ("py:class", "_pytest._code.code.TerminalRepr"), + ("py:class", "_pytest.fixtures.FixtureFunctionMarker"), + ("py:class", "_pytest.logging.LogCaptureHandler"), + ("py:class", "_pytest.mark.structures.ParameterSet"), + # Intentionally undocumented/private + ("py:class", "_pytest._code.code.Traceback"), + ("py:class", "_pytest._py.path.LocalPath"), + ("py:class", "_pytest.capture.CaptureResult"), + ("py:class", "_pytest.compat.NotSetType"), + ("py:class", "_pytest.python.PyCollector"), + ("py:class", "_pytest.python.PyobjMixin"), + ("py:class", "_pytest.python_api.RaisesContext"), + ("py:class", "_pytest.recwarn.WarningsChecker"), + ("py:class", "_pytest.reports.BaseReport"), + # Undocumented third parties + ("py:class", "_tracing.TagTracerSub"), + ("py:class", "warnings.WarningMessage"), + # Undocumented type aliases + ("py:class", "LEGACY_PATH"), + ("py:class", "_PluggyPlugin"), + # TypeVars + ("py:class", "_pytest._code.code.E"), + ("py:class", "_pytest.fixtures.FixtureFunction"), + ("py:class", "_pytest.nodes._NodeType"), + ("py:class", "_pytest.python_api.E"), + ("py:class", "_pytest.recwarn.T"), + ("py:class", "_pytest.runner.TResult"), + ("py:obj", "_pytest.fixtures.FixtureValue"), + ("py:obj", "_pytest.stash.T"), +] + + # -- Options for HTML output --------------------------------------------------- sys.path.append(os.path.abspath("_themes")) @@ -247,7 +291,7 @@ html_sidebars = { html_domain_indices = True # If false, no index is generated. -html_use_index = True +html_use_index = False # If true, the index is split into individual pages for each letter. # html_split_index = False @@ -320,7 +364,9 @@ latex_domain_indices = False # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)] +man_pages = [ + ("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1) +] # -- Options for Epub output --------------------------------------------------- @@ -338,7 +384,7 @@ epub_copyright = "2013, holger krekel et alii" # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' -# The unique identifier of the text. This can be a ISBN number +# The unique identifier of the text. This can be an ISBN number # or the project homepage. # epub_identifier = '' @@ -349,7 +395,7 @@ epub_copyright = "2013, holger krekel et alii" # The format is a list of tuples containing the path and title. # epub_pre_files = [] -# HTML files shat should be inserted after the pages created by sphinx. +# HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] @@ -382,7 +428,6 @@ texinfo_documents = [ ] -# Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "pluggy": ("https://pluggy.readthedocs.io/en/stable", None), "python": ("https://docs.python.org/3", None), @@ -390,19 +435,17 @@ intersphinx_mapping = { "pip": ("https://pip.pypa.io/en/stable", None), "tox": ("https://tox.wiki/en/stable", None), "virtualenv": ("https://virtualenv.pypa.io/en/stable", None), - "django": ( - "http://docs.djangoproject.com/en/stable", - "http://docs.djangoproject.com/en/stable/_objects", - ), "setuptools": ("https://setuptools.pypa.io/en/stable", None), + "packaging": ("https://packaging.python.org/en/latest", None), } def configure_logging(app: "sphinx.application.Sphinx") -> None: """Configure Sphinx's WarningHandler to handle (expected) missing include.""" - import sphinx.util.logging import logging + import sphinx.util.logging + class WarnLogFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: """Ignore warnings about missing include with "only" directive. @@ -422,8 +465,6 @@ def configure_logging(app: "sphinx.application.Sphinx") -> None: def setup(app: "sphinx.application.Sphinx") -> None: - # from sphinx.ext.autodoc import cut_lines - # app.connect('autodoc-process-docstring', cut_lines(4, what=['module'])) app.add_crossref_type( "fixture", "fixture", @@ -454,25 +495,6 @@ def setup(app: "sphinx.application.Sphinx") -> None: configure_logging(app) - # Make Sphinx mark classes with "final" when decorated with @final. - # We need this because we import final from pytest._compat, not from - # typing (for Python < 3.8 compat), so Sphinx doesn't detect it. - # To keep things simple we accept any `@final` decorator. - # Ref: https://github.com/pytest-dev/pytest/pull/7780 - import sphinx.pycode.ast - import sphinx.pycode.parser - - original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final - - def patched_is_final(self, decorators: List[ast.expr]) -> bool: - if original_is_final(self, decorators): - return True - return any( - sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators - ) - - sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final - # legacypath.py monkey-patches pytest.Testdir in. Import the file so # that autodoc can discover references to it. import _pytest.legacypath # noqa: F401 diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/contents.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/contents.rst index 049d44ba9d8..181207203b2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/contents.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/contents.rst @@ -44,7 +44,6 @@ How-to guides how-to/existingtestsuite how-to/unittest - how-to/nose how-to/xunit_setup how-to/bash-completion @@ -85,7 +84,6 @@ Further topics backwards-compatibility deprecations - py27-py34-deprecation contributing development_guide diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/deprecations.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/deprecations.rst index 0f19744adec..a65ea331663 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/deprecations.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/deprecations.rst @@ -16,25 +16,42 @@ Deprecated Features ------------------- Below is a complete list of all pytest features which are considered deprecated. Using those features will issue -:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. +:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`. -.. _instance-collector-deprecation: -The ``pytest.Instance`` collector -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _import-or-skip-import-error: -.. versionremoved:: 7.0 +``pytest.importorskip`` default behavior regarding :class:`ImportError` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``pytest.Instance`` collector type has been removed. +.. deprecated:: 8.2 -Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`. -Now :class:`~pytest.Class` collects the test methods directly. +Traditionally :func:`pytest.importorskip` will capture :class:`ImportError`, with the original intent being to skip +tests where a dependent module is not installed, for example testing with different dependencies. -Most plugins which reference ``Instance`` do so in order to ignore or skip it, -using a check such as ``if isinstance(node, Instance): return``. -Such plugins should simply remove consideration of ``Instance`` on pytest>=7. -However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``, -and importing it emits a deprecation warning. This will be removed in pytest 8. +However some packages might be installed in the system, but are not importable due to +some other issue, for example, a compilation error or a broken installation. In those cases :func:`pytest.importorskip` +would still silently skip the test, but more often than not users would like to see the unexpected +error so the underlying issue can be fixed. + +In ``8.2`` the ``exc_type`` parameter has been added, giving users the ability of passing :class:`ModuleNotFoundError` +to skip tests only if the module cannot really be found, and not because of some other error. + +Catching only :class:`ModuleNotFoundError` by default (and letting other errors propagate) would be the best solution, +however for backward compatibility, pytest will keep the existing behavior but raise an warning if: + +1. The captured exception is of type :class:`ImportError`, and: +2. The user does not pass ``exc_type`` explicitly. + +If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then the module is skipped and no +warning is emitted. + +This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with +users being able to supress the warning by passing ``exc_type=ImportError`` explicitly. + +In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture +:class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture +:class:`ImportError` by passing it to ``exc_type``. .. _node-ctor-fspath-deprecation: @@ -70,12 +87,54 @@ arguments they only pass on to the superclass. resolved in future versions as we slowly get rid of the :pypi:`py` dependency (see :issue:`9283` for a longer discussion). -Due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo` +Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo` which still is expected to return a ``py.path.local`` object, nodes still have both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, no matter what argument was used in the constructor. We expect to deprecate the ``fspath`` attribute in a future release. + +Configuring hook specs/impls using markers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before pluggy, pytest's plugin library, was its own package and had a clear API, +pytest just used ``pytest.mark`` to configure hooks. + +The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators +have been available since years and should be used instead. + +.. code-block:: python + + @pytest.mark.tryfirst + def pytest_runtest_call(): ... + + + # or + def pytest_runtest_call(): ... + + + pytest_runtest_call.tryfirst = True + +should be changed to: + +.. code-block:: python + + @pytest.hookimpl(tryfirst=True) + def pytest_runtest_call(): ... + +Changed ``hookimpl`` attributes: + +* ``tryfirst`` +* ``trylast`` +* ``optionalhook`` +* ``hookwrapper`` + +Changed ``hookwrapper`` attributes: + +* ``firstresult`` +* ``historic`` + + .. _legacy-path-hooks-deprecated: ``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` @@ -122,62 +181,6 @@ Directly constructing the following classes is now deprecated: These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8. -.. _cmdline-preparse-deprecated: - -Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 - -Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit` -is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these -functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument. - -.. code-block:: python - - def test_fail_example(): - # old - pytest.fail(msg="foo") - # new - pytest.fail(reason="bar") - - - def test_skip_example(): - # old - pytest.skip(msg="foo") - # new - pytest.skip(reason="bar") - - - def test_exit_example(): - # old - pytest.exit(msg="foo") - # new - pytest.exit(reason="bar") - - -Implementing the ``pytest_cmdline_preparse`` hook -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 - -Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated. -Implement the :hook:`pytest_load_initial_conftests` hook instead. - -.. code-block:: python - - def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: - ... - - - # becomes: - - - def pytest_load_initial_conftests( - early_config: Config, parser: Parser, args: List[str] - ) -> None: - ... - .. _diamond-inheritance-deprecated: Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item` @@ -185,7 +188,7 @@ Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item` .. deprecated:: 7.0 -Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning. +Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning. It was never sanely supported and triggers hard to debug errors. Some plugins providing linting/code analysis have been using this as a hack. @@ -197,8 +200,8 @@ Instead, a separate collector node should be used, which collects the item. See .. _uncooperative-constructors-deprecated: -Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.0 @@ -229,35 +232,245 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of ``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a deprecation warning is now raised. -Backward compatibilities in ``Parser.addoption`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Applying a mark to a fixture function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 2.4 +.. deprecated:: 7.4 -Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now -scheduled for removal in pytest 8 (deprecated since pytest 2.4.0): +Applying a mark to a fixture function never had any effect, but it is a common user error. -- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. -- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. +.. code-block:: python + + @pytest.mark.usefixtures("clean_database") + @pytest.fixture + def user() -> User: ... + +Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all. + +Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions. + + +Returning non-None value in test functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2 + +A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`. + +This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example: + +.. code-block:: python + + @pytest.mark.parametrize( + ["a", "b", "result"], + [ + [1, 2, 5], + [2, 3, 8], + [5, 3, 18], + ], + ) + def test_foo(a, b, result): + return foo(a, b) == result + +Given that pytest ignores the return value, this might be surprising that it will never fail. + +The proper fix is to change the `return` to an `assert`: + +.. code-block:: python + + @pytest.mark.parametrize( + ["a", "b", "result"], + [ + [1, 2, 5], + [2, 3, 8], + [5, 3, 18], + ], + ) + def test_foo(a, b, result): + assert foo(a, b) == result + + +The ``yield_fixture`` function/decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.2 + +``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`. + +It has been so for a very long time, so can be search/replaced safely. + + +Removed Features and Breaking Changes +------------------------------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + +Some breaking changes which could not be deprecated are also listed. + +.. _nose-deprecation: + +Support for tests written for nose +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2 +.. versionremoved:: 8.0 + +Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated. + +``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills +over the code base (see :issue:`9886` for more details). + +setup/teardown +^^^^^^^^^^^^^^ + +One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native, +they are in fact part of the ``nose`` support. + + +.. code-block:: python + class Test: + def setup(self): + self.resource = make_resource() -Raising ``unittest.SkipTest`` during collection -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def teardown(self): + self.resource.close() + + def test_foo(self): ... + + def test_bar(self): ... + + + +Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to: + +.. code-block:: python + + class Test: + def setup_method(self): + self.resource = make_resource() + + def teardown_method(self): + self.resource.close() + + def test_foo(self): ... + + def test_bar(self): ... + + +This is easy to do in an entire code base by doing a simple find/replace. + +@with_setup +^^^^^^^^^^^ + +Code using `@with_setup <with-setup-nose>`_ such as this: + +.. code-block:: python + + from nose.tools import with_setup + + + def setup_some_resource(): ... + + + def teardown_some_resource(): ... + + + @with_setup(setup_some_resource, teardown_some_resource) + def test_foo(): ... + +Will also need to be ported to a supported pytest style. One way to do it is using a fixture: + +.. code-block:: python + + import pytest + + + def setup_some_resource(): ... + + + def teardown_some_resource(): ... + + + @pytest.fixture + def some_resource(): + setup_some_resource() + yield + teardown_some_resource() + + + def test_foo(some_resource): ... + + +.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup + + +The ``compat_co_firstlineno`` attribute +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Nose inspects this attribute on function objects to allow overriding the function's inferred line number. +Pytest no longer respects this attribute. + + + +Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.0 +.. versionremoved:: 8.0 + +Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit` +is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these +functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument. + +.. code-block:: python + + def test_fail_example(): + # old + pytest.fail(msg="foo") + # new + pytest.fail(reason="bar") + + + def test_skip_example(): + # old + pytest.skip(msg="foo") + # new + pytest.skip(reason="bar") + + + def test_exit_example(): + # old + pytest.exit(msg="foo") + # new + pytest.exit(reason="bar") + + +.. _instance-collector-deprecation: + +The ``pytest.Instance`` collector +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Raising :class:`unittest.SkipTest` to skip collection of tests during the -pytest collection phase is deprecated. Use :func:`pytest.skip` instead. +.. versionremoved:: 7.0 + +The ``pytest.Instance`` collector type has been removed. + +Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`. +Now :class:`~pytest.Class` collects the test methods directly. + +Most plugins which reference ``Instance`` do so in order to ignore or skip it, +using a check such as ``if isinstance(node, Instance): return``. +Such plugins should simply remove consideration of ``Instance`` on pytest>=7. +However, to keep such uses working, a dummy type has been instanced in ``pytest.Instance`` and ``_pytest.python.Instance``, +and importing it emits a deprecation warning. This was removed in pytest 8. -Note: This deprecation only relates to using `unittest.SkipTest` during test -collection. You are probably not doing that. Ordinary usage of -:class:`unittest.SkipTest` / :meth:`unittest.TestCase.skipTest` / -:func:`unittest.skip` in unittest test cases is fully supported. Using ``pytest.warns(None)`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.0 +.. versionremoved:: 8.0 :func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused. Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()`` @@ -265,10 +478,25 @@ or ``pytest.warns(Warning)``. See :ref:`warns use cases` for examples. + +Backward compatibilities in ``Parser.addoption`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 2.4 +.. versionremoved:: 8.0 + +Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now +removed in pytest 8 (deprecated since pytest 2.4.0): + +- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead. +- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. + + The ``--strict`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 6.2 +.. versionremoved:: 8.0 The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which better conveys what the option does. @@ -278,39 +506,172 @@ flag for all strictness related options (``--strict-markers`` and ``--strict-con at the moment, more might be introduced in the future). -The ``yield_fixture`` function/decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _cmdline-preparse-deprecated: -.. deprecated:: 6.2 +Implementing the ``pytest_cmdline_preparse`` hook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`. +.. deprecated:: 7.0 +.. versionremoved:: 8.0 -It has been so for a very long time, so can be search/replaced safely. +Implementing the ``pytest_cmdline_preparse`` hook has been officially deprecated. +Implement the :hook:`pytest_load_initial_conftests` hook instead. +.. code-block:: python -The ``pytest_warning_captured`` hook -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ... -.. deprecated:: 6.0 -This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``. + # becomes: + + + def pytest_load_initial_conftests( + early_config: Config, parser: Parser, args: List[str] + ) -> None: ... + + +Collection changes in pytest 8 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Added a new :class:`pytest.Directory` base collection node, which all collector nodes for filesystem directories are expected to subclass. +This is analogous to the existing :class:`pytest.File` for file nodes. + +Changed :class:`pytest.Package` to be a subclass of :class:`pytest.Directory`. +A ``Package`` represents a filesystem directory which is a Python package, +i.e. contains an ``__init__.py`` file. + +:class:`pytest.Package` now only collects files in its own directory; previously it collected recursively. +Sub-directories are collected as sub-collector nodes, thus creating a collection tree which mirrors the filesystem hierarchy. + +:attr:`session.name <pytest.Session.name>` is now ``""``; previously it was the rootdir directory name. +This matches :attr:`session.nodeid <_pytest.nodes.Node.nodeid>` which has always been `""`. + +Added a new :class:`pytest.Dir` concrete collection node, a subclass of :class:`pytest.Directory`. +This node represents a filesystem directory, which is not a :class:`pytest.Package`, +i.e. does not contain an ``__init__.py`` file. +Similarly to ``Package``, it only collects the files in its own directory, +while collecting sub-directories as sub-collector nodes. + +Files and directories are now collected in alphabetical order jointly, unless changed by a plugin. +Previously, files were collected before directories. + +The collection tree now contains directories/packages up to the :ref:`rootdir <rootdir>`, +for initial arguments that are found within the rootdir. +For files outside the rootdir, only the immediate directory/package is collected -- +note however that collecting from outside the rootdir is discouraged. + +As an example, given the following filesystem tree:: + + myroot/ + pytest.ini + top/ + ├── aaa + │ └── test_aaa.py + ├── test_a.py + ├── test_b + │ ├── __init__.py + │ └── test_b.py + ├── test_c.py + └── zzz + ├── __init__.py + └── test_zzz.py + +the collection tree, as shown by `pytest --collect-only top/` but with the otherwise-hidden :class:`~pytest.Session` node added for clarity, +is now the following:: + + <Session> + <Dir myroot> + <Dir top> + <Dir aaa> + <Module test_aaa.py> + <Function test_it> + <Module test_a.py> + <Function test_it> + <Package test_b> + <Module test_b.py> + <Function test_it> + <Module test_c.py> + <Function test_it> + <Package zzz> + <Module test_zzz.py> + <Function test_it> + +Previously, it was:: + + <Session> + <Module top/test_a.py> + <Function test_it> + <Module top/test_c.py> + <Function test_it> + <Module top/aaa/test_aaa.py> + <Function test_it> + <Package test_b> + <Module test_b.py> + <Function test_it> + <Package zzz> + <Module test_zzz.py> + <Function test_it> + +Code/plugins which rely on a specific shape of the collection tree might need to update. + + +:class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionchanged:: 8.0 + +The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file. +Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module), +the module being the `__init__.py` file. +This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details). + +The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file. + +Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist, +if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files). + + +Collecting ``__init__.py`` files no longer collects package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 8.0 + +Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only. +Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself +(unless :confval:`python_files` was changed to allow `__init__.py` file). + +To collect the entire package, specify just the directory: `pytest pkg`. -Use the ``pytest_warning_recored`` hook instead, which replaces the ``item`` parameter -by a ``nodeid`` parameter. The ``pytest.collect`` module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 6.0 +.. versionremoved:: 7.0 The ``pytest.collect`` module is no longer part of the public API, all its names should now be imported from ``pytest`` directly instead. + +The ``pytest_warning_captured`` hook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0 +.. versionremoved:: 7.0 + +This hook has an `item` parameter which cannot be serialized by ``pytest-xdist``. + +Use the ``pytest_warning_recorded`` hook instead, which replaces the ``item`` parameter +by a ``nodeid`` parameter. + + + The ``pytest._fillfuncargs`` function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 6.0 +.. versionremoved:: 7.0 This function was kept for backward compatibility with an older plugin. @@ -319,12 +680,6 @@ it, use `function._request._fillfixtures()` instead, though note this is not a public API and may break in the future. -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. - ``--no-print-logs`` command-line option ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -410,7 +765,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading pytest 6.0, where the default format will be ``xunit2``. In order to let users know about the transition, pytest will issue a warning in case -the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly +the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. Services known to support the ``xunit2`` format: @@ -587,8 +942,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated (50, 500), ], ) - def test_foo(a, b): - ... + def test_foo(a, b): ... This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization call. @@ -611,8 +965,7 @@ To update the code, use ``pytest.param``: (50, 500), ], ) - def test_foo(a, b): - ... + def test_foo(a, b): ... .. _pytest_funcarg__ prefix deprecated: @@ -763,15 +1116,13 @@ This is just a matter of renaming the fixture as the API is the same: .. code-block:: python - def test_foo(record_xml_property): - ... + def test_foo(record_xml_property): ... Change to: .. code-block:: python - def test_foo(record_property): - ... + def test_foo(record_property): ... .. _passing command-line string to pytest.main deprecated: @@ -866,7 +1217,7 @@ that are then turned into proper test methods. Example: .. code-block:: python def check(x, y): - assert x ** x == y + assert x**x == y def test_squared(): @@ -881,7 +1232,7 @@ This form of test function doesn't support fixtures properly, and users should s @pytest.mark.parametrize("x, y", [(2, 4), (3, 9)]) def test_squared(x, y): - assert x ** x == y + assert x**x == y .. _internal classes accessed through node deprecated: @@ -933,8 +1284,7 @@ Example of usage: .. code-block:: python - class MySymbol: - ... + class MySymbol: ... def pytest_namespace(): diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py index abb9bce5097..f7a9c279426 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/failure_demo.py @@ -172,7 +172,7 @@ class TestRaises: raise ValueError("demo error") def test_tupleerror(self): - a, b = [1] # NOQA + a, b = [1] # noqa: F841 def test_reinterpret_fails_with_print_for_the_fun_of_it(self): items = [1, 2, 3] @@ -180,7 +180,7 @@ class TestRaises: a, b = items.pop() def test_some_error(self): - if namenotexi: # NOQA + if namenotexi: # noqa: F821 pass def func1(self): diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py index 7cdf18cdbc1..4aa7ec23bd1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/global_testmodule_config/conftest.py @@ -2,6 +2,7 @@ import os.path import pytest + mydir = os.path.dirname(__file__) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py index 350518b43c7..19d862f60b7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/assertion/test_failures.py @@ -1,6 +1,7 @@ import os.path import shutil + failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py") pytest_plugins = ("pytester",) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/attic.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/attic.rst index 2ea87006204..2b1f2766dce 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/attic.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/attic.rst @@ -25,7 +25,7 @@ example: specifying and selecting acceptance tests self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True) def run(self, *cmd): - """ called by test code to execute an acceptance test. """ + """called by test code to execute an acceptance test.""" self.tmpdir.chdir() return subprocess.check_output(cmd).decode() diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/conftest.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/conftest.py index f905738c4f6..66e70f14dd7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/conftest.py @@ -1 +1 @@ -collect_ignore = ["nonpython"] +collect_ignore = ["nonpython", "customdirectory"] diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory.rst new file mode 100644 index 00000000000..1e4d7e370de --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory.rst @@ -0,0 +1,77 @@ +.. _`custom directory collectors`: + +Using a custom directory collector +==================================================== + +By default, pytest collects directories using :class:`pytest.Package`, for directories with ``__init__.py`` files, +and :class:`pytest.Dir` for other directories. +If you want to customize how a directory is collected, you can write your own :class:`pytest.Directory` collector, +and use :hook:`pytest_collect_directory` to hook it up. + +.. _`directory manifest plugin`: + +A basic example for a directory manifest file +-------------------------------------------------------------- + +Suppose you want to customize how collection is done on a per-directory basis. +Here is an example ``conftest.py`` plugin that allows directories to contain a ``manifest.json`` file, +which defines how the collection should be done for the directory. +In this example, only a simple list of files is supported, +however you can imagine adding other keys, such as exclusions and globs. + +.. include:: customdirectory/conftest.py + :literal: + +You can create a ``manifest.json`` file and some test files: + +.. include:: customdirectory/tests/manifest.json + :literal: + +.. include:: customdirectory/tests/test_first.py + :literal: + +.. include:: customdirectory/tests/test_second.py + :literal: + +.. include:: customdirectory/tests/test_third.py + :literal: + +An you can now execute the test specification: + +.. code-block:: pytest + + customdirectory $ pytest + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project/customdirectory + configfile: pytest.ini + collected 2 items + + tests/test_first.py . [ 50%] + tests/test_second.py . [100%] + + ============================ 2 passed in 0.12s ============================= + +.. regendoc:wipe + +Notice how ``test_three.py`` was not executed, because it is not listed in the manifest. + +You can verify that your custom collector appears in the collection tree: + +.. code-block:: pytest + + customdirectory $ pytest --collect-only + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project/customdirectory + configfile: pytest.ini + collected 2 items + + <Dir customdirectory> + <ManifestDirectory tests> + <Module test_first.py> + <Function test_1> + <Module test_second.py> + <Function test_2> + + ======================== 2 tests collected in 0.12s ======================== diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/conftest.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/conftest.py new file mode 100644 index 00000000000..b2f68dba41a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/conftest.py @@ -0,0 +1,28 @@ +# content of conftest.py +import json + +import pytest + + +class ManifestDirectory(pytest.Directory): + def collect(self): + # The standard pytest behavior is to loop over all `test_*.py` files and + # call `pytest_collect_file` on each file. This collector instead reads + # the `manifest.json` file and only calls `pytest_collect_file` for the + # files defined there. + manifest_path = self.path / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + ihook = self.ihook + for file in manifest["files"]: + yield from ihook.pytest_collect_file( + file_path=self.path / file, parent=self + ) + + +@pytest.hookimpl +def pytest_collect_directory(path, parent): + # Use our custom collector for directories containing a `manifest.json` file. + if path.joinpath("manifest.json").is_file(): + return ManifestDirectory.from_parent(parent=parent, path=path) + # Otherwise fallback to the standard behavior. + return None diff --git a/tests/wpt/tests/tools/third_party/attrs/changelog.d/.gitignore b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/pytest.ini index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/attrs/changelog.d/.gitignore +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/pytest.ini diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/manifest.json b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/manifest.json new file mode 100644 index 00000000000..6ab6d0a5222 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/manifest.json @@ -0,0 +1,6 @@ +{ + "files": [ + "test_first.py", + "test_second.py" + ] +} diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_first.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_first.py new file mode 100644 index 00000000000..0a78de59945 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_first.py @@ -0,0 +1,3 @@ +# content of test_first.py +def test_1(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_second.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_second.py new file mode 100644 index 00000000000..eed724a7d96 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_second.py @@ -0,0 +1,3 @@ +# content of test_second.py +def test_2(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_third.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_third.py new file mode 100644 index 00000000000..61cf59dc16c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/customdirectory/tests/test_third.py @@ -0,0 +1,3 @@ +# content of test_third.py +def test_3(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py index b3512c2a64d..e76e3f93c62 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/fixtures/test_fixtures_order_dependencies.py @@ -17,7 +17,7 @@ def b(a, order): @pytest.fixture -def c(a, b, order): +def c(b, order): order.append("c") diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/index.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/index.rst index 71e855534ff..840819002d4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/index.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/index.rst @@ -18,7 +18,6 @@ For basic examples, see - :ref:`Fixtures <fixtures>` for basic fixture/setup examples - :ref:`parametrize` for basic test function parametrization - :ref:`unittest` for basic unittest integration -- :ref:`noseintegration` for basic nosetests integration The following examples aim at various use cases you might encounter. @@ -32,3 +31,4 @@ The following examples aim at various use cases you might encounter. special pythoncollection nonpython + customdirectory diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/markers.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/markers.rst index 3226c0871e0..c04d2a078dd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/markers.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/markers.rst @@ -45,7 +45,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 4 items / 3 deselected / 1 selected @@ -60,7 +60,7 @@ Or the inverse, running all tests except the webtest ones: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 4 items / 1 deselected / 3 selected @@ -82,7 +82,7 @@ tests based on their module, class, method, or function name: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 1 item @@ -97,7 +97,7 @@ You can also select on the class: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 1 item @@ -112,7 +112,7 @@ Or select multiple nodes: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 2 items @@ -136,7 +136,7 @@ Or select multiple nodes: Node IDs for failing tests are displayed in the test summary info when running pytest with the ``-rf`` option. You can also - construct Node IDs from the output of ``pytest --collectonly``. + construct Node IDs from the output of ``pytest --collect-only``. Using ``-k expr`` to select tests based on their name ------------------------------------------------------- @@ -156,7 +156,7 @@ The expression matching is now case-insensitive. $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 4 items / 3 deselected / 1 selected @@ -171,7 +171,7 @@ And you can also run all tests except the ones that match the keyword: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 4 items / 1 deselected / 3 selected @@ -188,7 +188,7 @@ Or to select "http" and "quick" tests: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 4 items / 2 deselected / 2 selected @@ -246,9 +246,9 @@ You can ask which markers exist for your test suite - the list includes our just @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead. For an example on how to add and work with markers from a plugin, see @@ -346,7 +346,7 @@ Custom marker and command line option to control test runs Plugins can provide custom markers and implement specific behaviour based on it. This is a self-contained example which adds a command line option and a parametrized test function marker to run tests -specifies via named environments: +specified via named environments: .. code-block:: python @@ -375,7 +375,7 @@ specifies via named environments: envnames = [mark.args[0] for mark in item.iter_markers(name="env")] if envnames: if item.config.getoption("-E") not in envnames: - pytest.skip("test requires env in {!r}".format(envnames)) + pytest.skip(f"test requires env in {envnames!r}") A test file using this local plugin: @@ -397,7 +397,7 @@ the test needs: $ pytest -E stage2 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -411,7 +411,7 @@ and here is one that specifies exactly the environment needed: $ pytest -E stage1 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -438,9 +438,9 @@ The ``--markers`` option always gives you a list of available markers: @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures - @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. + @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead. - @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. + @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead. .. _`passing callables to custom markers`: @@ -528,7 +528,7 @@ test function. From a conftest file we can read it like this: def pytest_runtest_setup(item): for mark in item.iter_markers(name="glob"): - print("glob args={} kwargs={}".format(mark.args, mark.kwargs)) + print(f"glob args={mark.args} kwargs={mark.kwargs}") sys.stdout.flush() Let's run this without capturing output and see what we get: @@ -558,6 +558,7 @@ for your particular platform, you could use the following plugin: # content of conftest.py # import sys + import pytest ALL = set("darwin linux win32".split()) @@ -567,7 +568,7 @@ for your particular platform, you could use the following plugin: supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform if supported_platforms and plat not in supported_platforms: - pytest.skip("cannot run on platform {}".format(plat)) + pytest.skip(f"cannot run on platform {plat}") then tests will be skipped if they were specified for a different platform. Let's do a little test file to show how this looks like: @@ -603,14 +604,14 @@ then you will see two tests skipped and two executed tests as expected: $ pytest -rs # this option reports skip reasons =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] conftest.py:12: cannot run on platform linux + SKIPPED [2] conftest.py:13: cannot run on platform linux ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -619,7 +620,7 @@ Note that if you specify a platform via the marker-command line option like this $ pytest -m linux =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items / 3 deselected / 1 selected @@ -682,7 +683,7 @@ We can now use the ``-m option`` to select one set: $ pytest -m interface --tb=short =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items / 2 deselected / 2 selected @@ -708,7 +709,7 @@ or to select both "event" and "interface" tests: $ pytest -m "interface or event" --tb=short =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items / 1 deselected / 3 selected diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/multipython.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/multipython.py index 9005d31addd..861ae9e528d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/multipython.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/multipython.py @@ -1,14 +1,14 @@ -""" -module containing a parametrized tests testing cross-python -serialization via the pickle module. -""" +"""Module containing a parametrized tests testing cross-python serialization +via the pickle module.""" + import shutil import subprocess import textwrap import pytest -pythonlist = ["python3.5", "python3.6", "python3.7"] + +pythonlist = ["python3.9", "python3.10", "python3.11"] @pytest.fixture(params=pythonlist) @@ -33,37 +33,33 @@ class Python: dumpfile = self.picklefile.with_name("dump.py") dumpfile.write_text( textwrap.dedent( - r""" + rf""" import pickle - f = open({!r}, 'wb') - s = pickle.dump({!r}, f, protocol=2) + f = open({str(self.picklefile)!r}, 'wb') + s = pickle.dump({obj!r}, f, protocol=2) f.close() - """.format( - str(self.picklefile), obj - ) + """ ) ) - subprocess.check_call((self.pythonpath, str(dumpfile))) + subprocess.run((self.pythonpath, str(dumpfile)), check=True) def load_and_is_true(self, expression): loadfile = self.picklefile.with_name("load.py") loadfile.write_text( textwrap.dedent( - r""" + rf""" import pickle - f = open({!r}, 'rb') + f = open({str(self.picklefile)!r}, 'rb') obj = pickle.load(f) f.close() - res = eval({!r}) + res = eval({expression!r}) if not res: raise SystemExit(1) - """.format( - str(self.picklefile), expression - ) + """ ) ) print(loadfile) - subprocess.check_call((self.pythonpath, str(loadfile))) + subprocess.run((self.pythonpath, str(loadfile)), check=True) @pytest.mark.parametrize("obj", [42, {}, {1: 3}]) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython.rst index f79f15b4f79..aa463e2416b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython.rst @@ -9,7 +9,7 @@ Working with non-python tests A basic example for specifying tests in Yaml files -------------------------------------------------------------- -.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py +.. _`pytest-yamlwsgi`: https://pypi.org/project/pytest-yamlwsgi/ Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests: @@ -28,7 +28,7 @@ now execute the test specification: nonpython $ pytest test_simple.yaml =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project/nonpython collected 2 items @@ -64,7 +64,7 @@ consulted when reporting in ``verbose`` mode: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project/nonpython collecting ... collected 2 items @@ -90,7 +90,7 @@ interesting to just look at the collection tree: nonpython $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project/nonpython collected 2 items diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py index bc39a1f6b20..e969e3e2518 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/nonpython/conftest.py @@ -12,7 +12,7 @@ class YamlFile(pytest.File): # We need a yaml parser, e.g. PyYAML. import yaml - raw = yaml.safe_load(self.path.open()) + raw = yaml.safe_load(self.path.open(encoding="utf-8")) for name, spec in sorted(raw.items()): yield YamlItem.from_parent(self, name=name, spec=spec) @@ -38,6 +38,7 @@ class YamlItem(pytest.Item): " no further details known at this point.", ] ) + return super().repr_failure(excinfo) def reportinfo(self): return self.path, 0, f"usecase: {self.name}" diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/parametrize.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/parametrize.rst index 66d72f3cc0d..03f6852e5c0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/parametrize.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/parametrize.rst @@ -4,8 +4,6 @@ Parametrizing tests ================================================= -.. currentmodule:: _pytest.python - ``pytest`` allows to easily parametrize test functions. For basic docs, see :ref:`parametrize-basics`. @@ -160,19 +158,20 @@ objects, they are still using the default pytest representation: $ pytest test_time.py --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 8 items - <Module test_time.py> - <Function test_timedistance_v0[a0-b0-expected0]> - <Function test_timedistance_v0[a1-b1-expected1]> - <Function test_timedistance_v1[forward]> - <Function test_timedistance_v1[backward]> - <Function test_timedistance_v2[20011212-20011211-expected0]> - <Function test_timedistance_v2[20011211-20011212-expected1]> - <Function test_timedistance_v3[forward]> - <Function test_timedistance_v3[backward]> + <Dir parametrize.rst-199> + <Module test_time.py> + <Function test_timedistance_v0[a0-b0-expected0]> + <Function test_timedistance_v0[a1-b1-expected1]> + <Function test_timedistance_v1[forward]> + <Function test_timedistance_v1[backward]> + <Function test_timedistance_v2[20011212-20011211-expected0]> + <Function test_timedistance_v2[20011211-20011212-expected1]> + <Function test_timedistance_v3[forward]> + <Function test_timedistance_v3[backward]> ======================== 8 tests collected in 0.12s ======================== @@ -185,7 +184,7 @@ A quick port of "testscenarios" Here is a quick port to run tests configured with :pypi:`testscenarios`, an add-on from Robert Collins for the standard unittest framework. We only have to work a bit to construct the correct arguments for pytest's -:py:func:`Metafunc.parametrize`: +:py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`: .. code-block:: python @@ -222,7 +221,7 @@ this is a fully self-contained example which you can run with: $ pytest test_scenarios.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items @@ -236,16 +235,17 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia $ pytest --collect-only test_scenarios.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items - <Module test_scenarios.py> - <Class TestSampleWithScenarios> - <Function test_demo1[basic]> - <Function test_demo2[basic]> - <Function test_demo1[advanced]> - <Function test_demo2[advanced]> + <Dir parametrize.rst-199> + <Module test_scenarios.py> + <Class TestSampleWithScenarios> + <Function test_demo1[basic]> + <Function test_demo2[basic]> + <Function test_demo1[advanced]> + <Function test_demo2[advanced]> ======================== 4 tests collected in 0.12s ======================== @@ -314,13 +314,14 @@ Let's first see how it looks like at collection time: $ pytest test_backends.py --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items - <Module test_backends.py> - <Function test_db_initialized[d1]> - <Function test_db_initialized[d2]> + <Dir parametrize.rst-199> + <Module test_backends.py> + <Function test_db_initialized[d1]> + <Function test_db_initialized[d2]> ======================== 2 tests collected in 0.12s ======================== @@ -412,7 +413,7 @@ The result of this test will be successful: $ pytest -v test_indirect_list.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 1 item @@ -483,8 +484,8 @@ argument sets to use for each test function. Let's run it: FAILED test_parametrize.py::TestClass::test_equals[1-2] - assert 1 == 2 1 failed, 2 passed in 0.12s -Indirect parametrization with multiple fixtures --------------------------------------------------------------- +Parametrization with multiple fixtures +-------------------------------------- Here is a stripped down real-life example of using parametrized testing for testing serialization of objects between different python @@ -502,15 +503,14 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - sssssssssssssssssssssssssss [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [9] multipython.py:29: 'python3.5' not found - SKIPPED [9] multipython.py:29: 'python3.6' not found - SKIPPED [9] multipython.py:29: 'python3.7' not found - 27 skipped in 0.12s + SKIPPED [12] multipython.py:65: 'python3.9' not found + SKIPPED [12] multipython.py:65: 'python3.11' not found + 3 passed, 24 skipped in 0.12s -Indirect parametrization of optional implementations/imports --------------------------------------------------------------------- +Parametrization of optional implementations/imports +--------------------------------------------------- If you want to compare the outcomes of several implementations of a given API, you can write test functions that receive the already imported implementations @@ -567,14 +567,14 @@ If you run this with reporting for skips enabled: $ pytest -rs test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2' + SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2' ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run @@ -628,7 +628,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: $ pytest -v -m basic =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 24 items / 21 deselected / 3 selected @@ -657,52 +657,34 @@ Use :func:`pytest.raises` with the :ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some tests raise exceptions and others do not. -It is helpful to define a no-op context manager ``does_not_raise`` to serve -as a complement to ``raises``. For example: +``contextlib.nullcontext`` can be used to test cases that are not expected to +raise exceptions but that should result in some value. The value is given as the +``enter_result`` parameter, which will be available as the ``with`` statement’s +target (``e`` in the example below). -.. code-block:: python +For example: - from contextlib import contextmanager - import pytest +.. code-block:: python + from contextlib import nullcontext - @contextmanager - def does_not_raise(): - yield + import pytest @pytest.mark.parametrize( "example_input,expectation", [ - (3, does_not_raise()), - (2, does_not_raise()), - (1, does_not_raise()), + (3, nullcontext(2)), + (2, nullcontext(3)), + (1, nullcontext(6)), (0, pytest.raises(ZeroDivisionError)), ], ) def test_division(example_input, expectation): """Test how much I know division.""" - with expectation: - assert (6 / example_input) is not None - -In the example above, the first three test cases should run unexceptionally, -while the fourth should raise ``ZeroDivisionError``. - -If you're only supporting Python 3.7+, you can simply use ``nullcontext`` -to define ``does_not_raise``: - -.. code-block:: python - - from contextlib import nullcontext as does_not_raise - -Or, if you're supporting Python 3.3+ you can use: - -.. code-block:: python - - from contextlib import ExitStack as does_not_raise - -Or, if desired, you can ``pip install contextlib2`` and use: - -.. code-block:: python + with expectation as e: + assert (6 / example_input) == e - from contextlib2 import nullcontext as does_not_raise +In the example above, the first three test cases should run without any +exceptions, while the fourth should raise a``ZeroDivisionError`` exception, +which is expected by pytest. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst index b9c2386ac51..aa9d05d7227 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/pythoncollection.rst @@ -147,14 +147,16 @@ The test collection would look like this: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y - rootdir: /home/sweet/project, configfile: pytest.ini + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + configfile: pytest.ini collected 2 items - <Module check_myapp.py> - <Class CheckMyApp> - <Function simple_check> - <Function complex_check> + <Dir pythoncollection.rst-200> + <Module check_myapp.py> + <Class CheckMyApp> + <Function simple_check> + <Function complex_check> ======================== 2 tests collected in 0.12s ======================== @@ -208,15 +210,18 @@ You can always peek at the collection tree without running tests like this: . $ pytest --collect-only pythoncollection.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y - rootdir: /home/sweet/project, configfile: pytest.ini + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + configfile: pytest.ini collected 3 items - <Module CWD/pythoncollection.py> - <Function test_function> - <Class TestClass> - <Function test_method> - <Function test_anothermethod> + <Dir pythoncollection.rst-200> + <Dir CWD> + <Module pythoncollection.py> + <Function test_function> + <Class TestClass> + <Function test_method> + <Function test_anothermethod> ======================== 3 tests collected in 0.12s ======================== @@ -289,8 +294,9 @@ file will be left out: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y - rootdir: /home/sweet/project, configfile: pytest.ini + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + configfile: pytest.ini collected 0 items ======================= no tests collected in 0.12s ======================== diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst index cab93143615..2c34cc2b00d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/reportingdemo.rst @@ -9,7 +9,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: assertion $ pytest failure_demo.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project/assertion collected 44 items @@ -80,6 +80,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_text(self): > assert "spam" == "eggs" E AssertionError: assert 'spam' == 'eggs' + E E - eggs E + spam @@ -91,6 +92,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_similar_text(self): > assert "foo 1 bar" == "foo 2 bar" E AssertionError: assert 'foo 1 bar' == 'foo 2 bar' + E E - foo 2 bar E ? ^ E + foo 1 bar @@ -104,6 +106,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_multiline_text(self): > assert "foo\nspam\nbar" == "foo\neggs\nbar" E AssertionError: assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + E E foo E - eggs E + spam @@ -119,6 +122,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: b = "1" * 100 + "b" + "2" * 100 > assert a == b E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222' + E E Skipping 90 identical leading characters in diff, use -v to show E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111b222222222 @@ -136,12 +140,12 @@ Here is a nice run of several failures and how ``pytest`` presents things: b = "1\n" * 100 + "b" + "2\n" * 100 > assert a == b E AssertionError: assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n...n2\n2\n2\n2\n' + E E Skipping 190 identical leading characters in diff, use -v to show E Skipping 191 identical trailing characters in diff, use -v to show E 1 E 1 E 1 - E 1 E 1... E E ...Full output truncated (7 lines hidden), use '-vv' to show @@ -154,8 +158,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_list(self): > assert [0, 1, 2] == [0, 1, 3] E assert [0, 1, 2] == [0, 1, 3] + E E At index 2 diff: 2 != 3 - E Use -v to get the full diff + E Use -v to get more diff failure_demo.py:63: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ @@ -167,8 +172,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: b = [0] * 100 + [2] + [3] * 100 > assert a == b E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...] + E E At index 100 diff: 1 != 2 - E Use -v to get the full diff + E Use -v to get more diff failure_demo.py:68: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ @@ -178,15 +184,15 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_dict(self): > assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0} E AssertionError: assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0} + E E Omitting 1 identical items, use -vv to show E Differing items: E {'b': 1} != {'b': 2} E Left contains 1 more item: E {'c': 0} E Right contains 1 more item: - E {'d': 0}... - E - E ...Full output truncated (2 lines hidden), use '-vv' to show + E {'d': 0} + E Use -v to get more diff failure_demo.py:71: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ @@ -195,16 +201,16 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_set(self): > assert {0, 10, 11, 12} == {0, 20, 21} - E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21} + E assert {0, 10, 11, 12} == {0, 20, 21} + E E Extra items in the left set: E 10 E 11 E 12 E Extra items in the right set: E 20 - E 21... - E - E ...Full output truncated (2 lines hidden), use '-vv' to show + E 21 + E Use -v to get more diff failure_demo.py:74: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ @@ -214,8 +220,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_eq_longer_list(self): > assert [1, 2] == [1, 2, 3] E assert [1, 2] == [1, 2, 3] + E E Right contains one more item: 3 - E Use -v to get the full diff + E Use -v to get more diff failure_demo.py:77: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ @@ -235,15 +242,15 @@ Here is a nice run of several failures and how ``pytest`` presents things: text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail" > assert "foo" not in text E AssertionError: assert 'foo' not in 'some multil...nand a\ntail' + E E 'foo' is contained here: E some multiline E text E which E includes foo E ? +++ - E and a... - E - E ...Full output truncated (2 lines hidden), use '-vv' to show + E and a + E tail failure_demo.py:84: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ @@ -254,6 +261,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: text = "single foo line" > assert "foo" not in text E AssertionError: assert 'foo' not in 'single foo line' + E E 'foo' is contained here: E single foo line E ? +++ @@ -267,6 +275,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: text = "head " * 50 + "foo " + "tail " * 20 > assert "foo" not in text E AssertionError: assert 'foo' not in 'head head h...l tail tail ' + E E 'foo' is contained here: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ @@ -280,6 +289,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: text = "head " * 50 + "f" * 70 + "tail " * 20 > assert "f" * 70 not in text E AssertionError: assert 'fffffffffff...ffffffffffff' not in 'head head h...l tail tail ' + E E 'ffffffffffffffffff...fffffffffffffffffff' is contained here: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -307,9 +317,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: E ['b'] E E Drill down into differing attribute b: - E b: 'b' != 'c'... - E - E ...Full output truncated (3 lines hidden), use '-vv' to show + E b: 'b' != 'c' + E - c + E + b failure_demo.py:108: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ @@ -334,9 +344,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: E ['b'] E E Drill down into differing attribute b: - E b: 'b' != 'c'... - E - E ...Full output truncated (3 lines hidden), use '-vv' to show + E b: 'b' != 'c' + E - c + E + b failure_demo.py:120: AssertionError ______________________________ test_attribute ______________________________ @@ -435,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: self = <failure_demo.TestRaises object at 0xdeadbeef0020> def test_tupleerror(self): - > a, b = [1] # NOQA + > a, b = [1] # noqa: F841 E ValueError: not enough values to unpack (expected 2, got 1) failure_demo.py:175: ValueError @@ -457,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: self = <failure_demo.TestRaises object at 0xdeadbeef0022> def test_some_error(self): - > if namenotexi: # NOQA + > if namenotexi: # noqa: F821 E NameError: name 'namenotexi' is not defined failure_demo.py:183: NameError @@ -673,7 +683,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser... - FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert... + FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/simple.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/simple.rst index a70f3404992..7064f61f0e2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/simple.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/simple.rst @@ -168,7 +168,7 @@ Now we'll get feedback on a bad argument: If you need to provide more detailed error messages, you can use the -``type`` parameter and raise ``pytest.UsageError``: +``type`` parameter and raise :exc:`pytest.UsageError`: .. code-block:: python @@ -232,7 +232,7 @@ directory with the above conftest.py: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 0 items @@ -296,7 +296,7 @@ and when running it will see a skipped "slow" test: $ pytest -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -312,7 +312,7 @@ Or run it including the ``slow`` marked test: $ pytest --runslow =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -342,7 +342,7 @@ Example: def checkconfig(x): __tracebackhide__ = True if not hasattr(x, "config"): - pytest.fail("not configured: {}".format(x)) + pytest.fail(f"not configured: {x}") def test_something(): @@ -376,6 +376,7 @@ this to make sure unexpected exception types aren't hidden: .. code-block:: python import operator + import pytest @@ -386,7 +387,7 @@ this to make sure unexpected exception types aren't hidden: def checkconfig(x): __tracebackhide__ = operator.methodcaller("errisinstance", ConfigException) if not hasattr(x, "config"): - raise ConfigException("not configured: {}".format(x)) + raise ConfigException(f"not configured: {x}") def test_something(): @@ -455,7 +456,7 @@ which will add the string to the test header accordingly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y project deps: mylib-1.1 rootdir: /home/sweet/project collected 0 items @@ -483,7 +484,7 @@ which will add info only when run with "--v": $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache info1: did you know that ... did you? @@ -498,7 +499,7 @@ and nothing when run plainly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 0 items @@ -537,7 +538,7 @@ Now we can profile which test functions execute the slowest: $ pytest --durations=3 =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 3 items @@ -565,6 +566,7 @@ an ``incremental`` marker which is to be used on classes: # content of conftest.py from typing import Dict, Tuple + import pytest # store history of failures per test class name and per index in parametrize (if parametrize used) @@ -608,7 +610,7 @@ an ``incremental`` marker which is to be used on classes: test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) # if name found, test has failed for the combination of class name & test name if test_name is not None: - pytest.xfail("previous test failed ({})".format(test_name)) + pytest.xfail(f"previous test failed ({test_name})") These two hook implementations work together to abort incremental-marked @@ -642,7 +644,7 @@ If we run this: $ pytest -rx =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 4 items @@ -658,9 +660,33 @@ If we run this: E assert 0 test_step.py:11: AssertionError + ================================ XFAILURES ================================= + ______________________ TestUserHandling.test_deletion ______________________ + + item = <Function test_deletion> + + def pytest_runtest_setup(item): + if "incremental" in item.keywords: + # retrieve the class name of the test + cls_name = str(item.cls) + # check if a previous test has failed for this class + if cls_name in _test_failed_incremental: + # retrieve the index of the test (if parametrize is used in combination with incremental) + parametrize_index = ( + tuple(item.callspec.indices.values()) + if hasattr(item, "callspec") + else () + ) + # retrieve the name of the first test function to fail for this class name and index + test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) + # if name found, test has failed for the combination of class name & test name + if test_name is not None: + > pytest.xfail(f"previous test failed ({test_name})") + E _pytest.outcomes.XFailed: previous test failed (test_modification) + + conftest.py:47: XFailed ========================= short test summary info ========================== - XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification) + XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification) ================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== We'll see that ``test_deletion`` was not executed because ``test_modification`` @@ -690,7 +716,7 @@ Here is an example for making a ``db`` fixture available in a directory: pass - @pytest.fixture(scope="session") + @pytest.fixture(scope="package") def db(): return DB() @@ -725,14 +751,14 @@ We can run this: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 7 items - test_step.py .Fx. [ 57%] - a/test_db.py F [ 71%] - a/test_db2.py F [ 85%] - b/test_error.py E [100%] + a/test_db.py F [ 14%] + a/test_db2.py F [ 28%] + b/test_error.py E [ 42%] + test_step.py .Fx. [100%] ================================== ERRORS ================================== _______________________ ERROR at setup of test_root ________________________ @@ -744,39 +770,39 @@ We can run this: /home/sweet/project/b/test_error.py:1 ================================= FAILURES ================================= - ____________________ TestUserHandling.test_modification ____________________ - - self = <test_step.TestUserHandling object at 0xdeadbeef0002> - - def test_modification(self): - > assert 0 - E assert 0 - - test_step.py:11: AssertionError _________________________________ test_a1 __________________________________ - db = <conftest.DB object at 0xdeadbeef0003> + db = <conftest.DB object at 0xdeadbeef0002> def test_a1(db): > assert 0, db # to show value - E AssertionError: <conftest.DB object at 0xdeadbeef0003> + E AssertionError: <conftest.DB object at 0xdeadbeef0002> E assert 0 a/test_db.py:2: AssertionError _________________________________ test_a2 __________________________________ - db = <conftest.DB object at 0xdeadbeef0003> + db = <conftest.DB object at 0xdeadbeef0002> def test_a2(db): > assert 0, db # to show value - E AssertionError: <conftest.DB object at 0xdeadbeef0003> + E AssertionError: <conftest.DB object at 0xdeadbeef0002> E assert 0 a/test_db2.py:2: AssertionError + ____________________ TestUserHandling.test_modification ____________________ + + self = <test_step.TestUserHandling object at 0xdeadbeef0003> + + def test_modification(self): + > assert 0 + E assert 0 + + test_step.py:11: AssertionError ========================= short test summary info ========================== - FAILED test_step.py::TestUserHandling::test_modification - assert 0 FAILED a/test_db.py::test_a1 - AssertionError: <conftest.DB object at 0x7... FAILED a/test_db2.py::test_a2 - AssertionError: <conftest.DB object at 0x... + FAILED test_step.py::TestUserHandling::test_modification - assert 0 ERROR b/test_error.py::test_root ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12s ============== @@ -802,20 +828,20 @@ case we just write some information out to a ``failures`` file: # content of conftest.py - import pytest import os.path + import pytest + - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.failed: mode = "a" if os.path.exists("failures") else "w" - with open("failures", mode) as f: + with open("failures", mode, encoding="utf-8") as f: # let's also access a fixture for the fun of it if "tmp_path" in item.fixturenames: extra = " ({})".format(item.funcargs["tmp_path"]) @@ -824,6 +850,8 @@ case we just write some information out to a ``failures`` file: f.write(rep.nodeid + extra + "\n") + return rep + if you then have failing tests: @@ -843,7 +871,7 @@ and run them: $ pytest test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -890,20 +918,23 @@ here is a little example implemented via a local plugin: .. code-block:: python # content of conftest.py - + from typing import Dict import pytest + from pytest import StashKey, CollectReport + phase_report_key = StashKey[Dict[str, CollectReport]]() - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield - # set a report attribute for each phase of a call, which can + # store test results for each phase of a call, which can # be "setup", "call", "teardown" + item.stash.setdefault(phase_report_key, {})[rep.when] = rep - setattr(item, "rep_" + rep.when, rep) + return rep @pytest.fixture @@ -911,11 +942,11 @@ here is a little example implemented via a local plugin: yield # request.node is an "item" because we use the default # "function" scope - if request.node.rep_setup.failed: - print("setting up a test failed!", request.node.nodeid) - elif request.node.rep_setup.passed: - if request.node.rep_call.failed: - print("executing test failed", request.node.nodeid) + report = request.node.stash[phase_report_key] + if report["setup"].failed: + print("setting up a test failed or skipped", request.node.nodeid) + elif ("call" not in report) or report["call"].failed: + print("executing test failed or skipped", request.node.nodeid) if you then have failing tests: @@ -949,12 +980,12 @@ and run it: $ pytest -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 3 items - test_module.py Esetting up a test failed! test_module.py::test_setup_fails - Fexecuting test failed test_module.py::test_call_fails + test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails + Fexecuting test failed or skipped test_module.py::test_call_fails F ================================== ERRORS ================================== @@ -1066,6 +1097,7 @@ like ``pytest-timeout`` they must be imported explicitly and passed on to pytest # contents of app_main.py import sys + import pytest_timeout # Third party plugin if len(sys.argv) > 1 and sys.argv[1] == "--pytest": @@ -1083,4 +1115,4 @@ application with standard ``pytest`` command-line options: .. code-block:: bash - ./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/ + ./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/ diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py index 01e6da1ad2e..1040c89298d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/example/xfail_demo.py @@ -1,5 +1,6 @@ import pytest + xfail = pytest.mark.xfail diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst index e86dd74251e..93d3400dae2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/anatomy.rst @@ -34,7 +34,7 @@ a function/method call. **Assert** is where we look at that resulting state and check if it looks how we'd expect after the dust has settled. It's where we gather evidence to say the -behavior does or does not aligns with what we expect. The ``assert`` in our test +behavior does or does not align with what we expect. The ``assert`` in our test is where we take that measurement/observation and apply our judgement to it. If something should be green, we'd say ``assert thing == "green"``. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst index 194e576493e..0bb3bf49fb0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/fixtures.rst @@ -85,7 +85,7 @@ style of setup/teardown functions: In addition, pytest continues to support :ref:`xunitsetup`. You can mix both styles, moving incrementally from classic to new style, as you prefer. You can also start out from existing :ref:`unittest.TestCase -style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects. +style <unittest.TestCase>`. @@ -162,7 +162,7 @@ A note about fixture cleanup ---------------------------- pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and -:data:`SIGQUIT <signal.SIGQUIT>` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally +``SIGQUIT`` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important to be cleared when the Python process is terminated (by those signals) might leak resources. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst index 50121c7a761..41cbe847989 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/flaky.rst @@ -52,10 +52,9 @@ Plugins Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this: -* `flaky <https://github.com/box/flaky>`_ -* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_ * `pytest-rerunfailures <https://github.com/pytest-dev/pytest-rerunfailures>`_ * `pytest-replay <https://github.com/ESSS/pytest-replay>`_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs. +* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_ Plugins to deliberately randomize tests can help expose tests with state problems: @@ -94,7 +93,7 @@ Mark Lapierre discusses the `Pros and Cons of Quarantined Tests <https://dev.to/ CI tools that rerun on failure ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/azure/devops/release-notes/2017/dec-11-vsts#identify-flaky-tests>`_ and rerun failed tests. +Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/previous-versions/azure/devops/2017/dec-11-vsts?view=tfs-2017#identify-flaky-tests>`_ and rerun failed tests. @@ -106,7 +105,7 @@ This is a limited list, please submit an issue or pull request to expand it! * Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF <http://www.cs.umd.edu/~atif/pubs/gao-icse15.pdf>`__ * Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive <https://drive.google.com/file/d/10HdcCQiuQVgW3yYUJD-TSTq1NbYEprl0/view>`__ * Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF <https://www.jonbell.net/icse18-deflaker.pdf>`__ - +* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF <https://www.cs.cornell.edu/~saikatd/papers/flash-issta20.pdf>`__ Resources ^^^^^^^^^ diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst index 32a14991ae2..1390ba4e8fe 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/goodpractices.rst @@ -12,41 +12,27 @@ For development, we recommend you use :mod:`venv` for virtual environments and as well as the ``pytest`` package itself. This ensures your code and dependencies are isolated from your system Python installation. -Next, place a ``pyproject.toml`` file in the root of your package: +Create a ``pyproject.toml`` file in the root of your repository as described in +:doc:`packaging:tutorials/packaging-projects`. +The first few lines should look like this: .. code-block:: toml [build-system] - requires = ["setuptools>=42", "wheel"] - build-backend = "setuptools.build_meta" + requires = ["hatchling"] + build-backend = "hatchling.build" -and a ``setup.cfg`` file containing your package's metadata with the following minimum content: + [project] + name = "PACKAGENAME" + version = "PACKAGEVERSION" -.. code-block:: ini - - [metadata] - name = PACKAGENAME - - [options] - packages = find: - -where ``PACKAGENAME`` is the name of your package. - -.. note:: - - If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file: - - .. code-block:: python - - from setuptools import setup - - setup() +where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively. You can then install your package in "editable" mode by running from the same directory: .. code-block:: bash - pip install -e . + pip install -e . which lets you change your source code (both tests and application) and rerun tests at will. @@ -65,8 +51,8 @@ Conventions for Python test discovery * In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_. * From those files, collect test items: - * ``test`` prefixed test functions or methods outside of class - * ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method) + * ``test`` prefixed test functions or methods outside of class. + * ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered. For examples of how to customize your test discovery :doc:`/example/pythoncollection`. @@ -74,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard :ref:`unittest.TestCase <unittest.TestCase>` subclassing technique. -Choosing a test layout / import rules -------------------------------------- +.. _`test layout`: + +Choosing a test layout +---------------------- ``pytest`` supports two common test layouts: @@ -89,11 +77,11 @@ to keep tests separate from actual application code (often a good idea): .. code-block:: text pyproject.toml - setup.cfg - mypkg/ - __init__.py - app.py - view.py + src/ + mypkg/ + __init__.py + app.py + view.py tests/ test_app.py test_view.py @@ -103,83 +91,56 @@ This has the following benefits: * Your tests can run against an installed version after executing ``pip install .``. * Your tests can run against the local copy with an editable install after executing ``pip install --editable .``. -* If you don't use an editable install and are relying on the fact that Python by default puts the current - directory in ``sys.path`` to import your package, you can execute ``python -m pytest`` to execute the tests against the - local copy directly, without using ``pip``. -.. note:: +For new projects, we recommend to use ``importlib`` :ref:`import mode <import-modes>` +(see which-import-mode_ for a detailed explanation). +To this end, add the following to your ``pyproject.toml``: - See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and - ``python -m pytest``. +.. code-block:: toml -Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode <import-modes>` -(which is the default): your test files must have **unique names**, because -``pytest`` will import them as *top-level* modules since there are no packages -to derive a full package name from. In other words, the test files in the example above will -be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to -``sys.path``. + [tool.pytest.ini_options] + addopts = [ + "--import-mode=importlib", + ] -If you need to have test modules with the same name, you might add ``__init__.py`` files to your -``tests`` folder and subfolders, changing them to packages: +.. _src-layout: -.. code-block:: text +Generally, but especially if you use the default import mode ``prepend``, +it is **strongly** suggested to use a ``src`` layout. +Here, your application root package resides in a sub-directory of your root, +i.e. ``src/mypkg/`` instead of ``mypkg``. - pyproject.toml - setup.cfg - mypkg/ - ... - tests/ - __init__.py - foo/ - __init__.py - test_view.py - bar/ - __init__.py - test_view.py +This layout prevents a lot of common pitfalls and has many benefits, +which are better explained in this excellent `blog post`_ by Ionel Cristian Mărieș. -Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, allowing -you to have modules with the same name. But now this introduces a subtle problem: in order to load -the test modules from the ``tests`` directory, pytest prepends the root of the repository to -``sys.path``, which adds the side-effect that now ``mypkg`` is also importable. +.. _blog post: https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure> -This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment, -because you want to test the *installed* version of your package, not the local code from the repository. +.. note:: -.. _`src-layout`: + If you do not use an editable install and use the ``src`` layout as above you need to extend the Python's + search path for module files to execute the tests against the local copy directly. You can do it in an + ad-hoc manner by setting the ``PYTHONPATH`` environment variable: -In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a -sub-directory of your root: + .. code-block:: bash -.. code-block:: text + PYTHONPATH=src pytest - pyproject.toml - setup.cfg - src/ - mypkg/ - __init__.py - app.py - view.py - tests/ - __init__.py - foo/ - __init__.py - test_view.py - bar/ - __init__.py - test_view.py + or in a permanent manner by using the :confval:`pythonpath` configuration variable and adding the + following to your ``pyproject.toml``: + .. code-block:: toml -This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent -`blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_. + [tool.pytest.ini_options] + pythonpath = "src" .. note:: - The new ``--import-mode=importlib`` (see :ref:`import-modes`) doesn't have - any of the drawbacks above because ``sys.path`` is not changed when importing - test modules, so users that run - into this issue are strongly encouraged to try it and report if the new option works well for them. - The ``src`` directory layout is still strongly recommended however. + If you do not use an editable install and not use the ``src`` layout (``mypkg`` directly in the root + directory) you can rely on the fact that Python by default puts the current directory in ``sys.path`` to + import your package and run ``python -m pytest`` to execute the tests against the local copy directly. + See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and + ``python -m pytest``. Tests as part of application code ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -191,12 +152,11 @@ want to distribute them along with your application: .. code-block:: text pyproject.toml - setup.cfg - mypkg/ + [src/]mypkg/ __init__.py app.py view.py - test/ + tests/ __init__.py test_app.py test_view.py @@ -254,6 +214,56 @@ Note that this layout also works in conjunction with the ``src`` layout mentione much less surprising. +.. _which-import-mode: + +Choosing an import mode +^^^^^^^^^^^^^^^^^^^^^^^ + +For historical reasons, pytest defaults to the ``prepend`` :ref:`import mode <import-modes>` +instead of the ``importlib`` import mode we recommend for new projects. +The reason lies in the way the ``prepend`` mode works: + +Since there are no packages to derive a full package name from, +``pytest`` will import your test files as *top-level* modules. +The test files in the first example (:ref:`src layout <src-layout>`) would be imported as +``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to ``sys.path``. + +This results in a drawback compared to the import mode ``importlib``: +your test files must have **unique names**. + +If you need to have test modules with the same name, +as a workaround you might add ``__init__.py`` files to your ``tests`` folder and subfolders, +changing them to packages: + +.. code-block:: text + + pyproject.toml + mypkg/ + ... + tests/ + __init__.py + foo/ + __init__.py + test_view.py + bar/ + __init__.py + test_view.py + +Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, +allowing you to have modules with the same name. +But now this introduces a subtle problem: +in order to load the test modules from the ``tests`` directory, +pytest prepends the root of the repository to ``sys.path``, +which adds the side-effect that now ``mypkg`` is also importable. + +This is problematic if you are using a tool like tox_ to test your package in a virtual environment, +because you want to test the *installed* version of your package, +not the local code from the repository. + +The ``importlib`` import mode does not have any of the drawbacks above, +because ``sys.path`` is not changed when importing test modules. + + .. _`buildout`: http://www.buildout.org/en/latest/ .. _`use tox`: @@ -263,8 +273,8 @@ tox Once you are done with your work and want to make sure that your actual package passes all tests you may want to look into :doc:`tox <tox:index>`, the -virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`. -tox helps you to setup virtualenv environments with pre-defined +virtualenv test automation tool. +``tox`` helps you to setup virtualenv environments with pre-defined dependencies and then executing a pre-configured test command with options. It will run tests against the installed package and not against your source code checkout, helping to detect packaging @@ -286,3 +296,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>` setuptools intends to `remove the test command <https://github.com/pypa/setuptools/issues/931>`_. + +Checking with flake8-pytest-style +--------------------------------- + +In order to ensure that pytest is being used correctly in your project, +it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin. + +flake8-pytest-style checks for common mistakes and coding style violations in pytest code, +such as incorrect use of fixtures, test function names, and markers. +By using this plugin, you can catch these errors early in the development process +and ensure that your pytest code is consistent and easy to maintain. + +A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_. + +.. note:: + + flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst index 2330356b863..33eba86b57a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/explanation/pythonpath.rst @@ -10,21 +10,29 @@ Import modes pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution. -Importing files in Python (at least until recently) is a non-trivial processes, often requiring -changing :data:`sys.path`. Some aspects of the +Importing files in Python is a non-trivial processes, so aspects of the import process can be controlled through the ``--import-mode`` command-line flag, which can assume these values: +.. _`import-mode-prepend`: + * ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* - of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin. + of :py:data:`sys.path` if not already there, and then imported with + the :func:`importlib.import_module <importlib.import_module>` function. + + It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories + containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full + name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package). - This requires test module names to be unique when the test directory tree is not arranged in - packages, because the modules will put in :py:data:`sys.modules` after importing. + If the test directory tree is not arranged as packages, then each test file needs to have a unique name + compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name. This is the classic mechanism, dating back from the time Python 2 was still supported. +.. _`import-mode-append`: + * ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already - there, and imported with ``__import__``. + there, and imported with :func:`importlib.import_module <importlib.import_module>`. This better allows to run test modules against installed versions of a package even if the package under test has the same import root. For example: @@ -38,23 +46,78 @@ these values: the tests will run against the installed version of ``pkg_under_test`` when ``--import-mode=append`` is used whereas with ``prepend`` they would pick up the local version. This kind of confusion is why - we advocate for using :ref:`src <src-layout>` layouts. + we advocate for using :ref:`src-layouts <src-layout>`. Same as ``prepend``, requires test module names to be unique when the test directory tree is not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing. -* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`. +.. _`import-mode-importlib`: + +* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`. + + Advantages of this mode: + + * pytest will not change :py:data:`sys.path` at all. + * Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``. + + Disadvantages: + + * Test modules can't import each other. + * Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes) + are not importable. The recommendation in this case it to place testing utility modules together with the application/library + code, for example ``app.testing.helpers``. + + Important: by "test utility modules" we mean functions/classes which are imported by + other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along + with the test modules, and are discovered automatically by pytest. + + It works like this: + + 1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name + like ``tests.core.test_models`` and tries to import it. + + For non-test modules this will work if they are accessible via :py:data:`sys.path`, so + for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``. + This is happens when plugins import non-test modules (for example doctesting). + + If this step succeeds, the module is returned. + + For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail. + + 2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without + changing :py:data:`sys.path`. + + Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based + on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`. + + For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``. + + .. versionadded:: 6.0 + +.. note:: + + Initially we intended to make ``importlib`` the default in future releases, however it is clear now that + it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future. + +.. note:: + + By default, pytest will not attempt to resolve namespace packages automatically, but that can + be changed via the :confval:`consider_namespace_packages` configuration variable. + +.. seealso:: + + The :confval:`pythonpath` configuration variable. + + The :confval:`consider_namespace_packages` configuration variable. - For this reason this doesn't require test module names to be unique, but also makes test - modules non-importable by each other. + :ref:`test layout`. - We intend to make ``importlib`` the default in future releases, depending on feedback. ``prepend`` and ``append`` import modes scenarios ------------------------------------------------- Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to -change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users +change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users might encounter because of that. Test modules / ``conftest.py`` files inside packages @@ -83,7 +146,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the last folder which still contains an ``__init__.py`` file in order to find the package *root* (in this case ``foo/``). To load the module, it will insert ``root/`` to the front of -``sys.path`` (if not there already) in order to load +:py:data:`sys.path` (if not there already) in order to load ``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``. The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module. @@ -113,8 +176,8 @@ When executing: pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to -``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done -with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``. +:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done +with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``. For this reason this layout cannot have test modules with the same name, as they all will be imported in the global import namespace. @@ -127,7 +190,7 @@ Invoking ``pytest`` versus ``python -m pytest`` ----------------------------------------------- Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly -equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which +equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which is standard ``python`` behavior. See also :ref:`invoke-python`. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst index 3bf4527cfb5..8b900d30f20 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/funcarg_compare.rst @@ -11,8 +11,6 @@ funcarg mechanism, see :ref:`historical funcargs and pytest.funcargs`. If you are new to pytest, then you can simply ignore this section and read the other sections. -.. currentmodule:: _pytest - Shortcomings of the previous ``pytest_funcarg__`` mechanism -------------------------------------------------------------- @@ -46,7 +44,7 @@ There are several limitations and difficulties with this approach: 2. parametrizing the "db" resource is not straight forward: you need to apply a "parametrize" decorator or implement a - :py:func:`~hookspec.pytest_generate_tests` hook + :hook:`pytest_generate_tests` hook calling :py:func:`~pytest.Metafunc.parametrize` which performs parametrization at the places where the resource is used. Moreover, you need to modify the factory to use an @@ -94,15 +92,14 @@ Direct parametrization of funcarg resource factories Previously, funcarg factories could not directly cause parametrization. You needed to specify a ``@parametrize`` decorator on your test function -or implement a ``pytest_generate_tests`` hook to perform +or implement a :hook:`pytest_generate_tests` hook to perform parametrization, i.e. calling a test multiple times with different value sets. pytest-2.3 introduces a decorator for use on the factory itself: .. code-block:: python @pytest.fixture(params=["mysql", "pg"]) - def db(request): - ... # use request.param + def db(request): ... # use request.param Here the factory will be invoked twice (with the respective "mysql" and "pg" values set as ``request.param`` attributes) and all of @@ -143,8 +140,7 @@ argument: .. code-block:: python @pytest.fixture() - def db(request): - ... + def db(request): ... The name under which the funcarg resource can be requested is ``db``. @@ -153,8 +149,7 @@ aka: .. code-block:: python - def pytest_funcarg__db(request): - ... + def pytest_funcarg__db(request): ... But it is then not possible to define scoping and parametrization. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/getting-started.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/getting-started.rst index 5d13a768069..94e0d80e656 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/getting-started.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/getting-started.rst @@ -9,7 +9,7 @@ Get Started Install ``pytest`` ---------------------------------------- -``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. 1. Run the following command in your command line: @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 7.0.1 + pytest 8.2.1 .. _`simpletest`: @@ -47,7 +47,7 @@ The test $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -97,6 +97,30 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e with pytest.raises(SystemExit): f() +You can also use the context provided by :ref:`raises <assertraises>` to +assert that an expected exception is part of a raised :class:`ExceptionGroup`: + +.. code-block:: python + + # content of test_exceptiongroup.py + import pytest + + + def f(): + raise ExceptionGroup( + "Group message", + [ + RuntimeError(), + ], + ) + + + def test_exception_in_group(): + with pytest.raises(ExceptionGroup) as excinfo: + f() + assert excinfo.group_contains(RuntimeError) + assert not excinfo.group_contains(TypeError) + Execute the test function with “quiet” reporting mode: .. code-block:: pytest @@ -250,7 +274,7 @@ Continue reading Check out additional pytest resources to help you customize tests for your unique workflow: * ":ref:`usage`" for command line invocation examples -* ":ref:`existingtestsuite`" for working with pre-existing tests +* ":ref:`existingtestsuite`" for working with preexisting tests * ":ref:`mark`" for information on the ``pytest.mark`` mechanism * ":ref:`fixtures`" for providing a functional baseline to your tests * ":ref:`plugins`" for managing and writing plugins diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/historical-notes.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/historical-notes.rst index 29ebbd5d199..5eb527c582b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/historical-notes.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/historical-notes.rst @@ -112,7 +112,7 @@ More details can be found in the :pull:`original PR <3317>`. .. note:: in a future major release of pytest we will introduce class based markers, - at which point markers will no longer be limited to instances of :py:class:`~_pytest.mark.Mark`. + at which point markers will no longer be limited to instances of :py:class:`~pytest.Mark`. cache plugin integrated into the core @@ -227,8 +227,7 @@ to use strings: @pytest.mark.skipif("sys.version_info >= (3,3)") - def test_function(): - ... + def test_function(): ... During test function setup the skipif condition is evaluated by calling ``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains @@ -262,8 +261,7 @@ configuration value which you might have added: .. code-block:: python @pytest.mark.skipif("not config.getvalue('db')") - def test_function(): - ... + def test_function(): ... The equivalent with "boolean conditions" is: diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/assert.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/assert.rst index cb70db6b8ed..7b027744695 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/assert.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/assert.rst @@ -29,7 +29,7 @@ you will see the return value of the function call: $ pytest test_assert1.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the idiomatic python constructs without boilerplate code while not losing introspection information. -However, if you specify a message with the assertion like this: +If a message is specified with the assertion like this: .. code-block:: python assert a % 2 == 0, "value was odd, should be even" -then no assertion introspection takes places at all and the message -will be simply shown in the traceback. +it is printed alongside the assertion introspection in the traceback. See :ref:`assert-details` for more information on assertion introspection. @@ -99,6 +98,27 @@ and if you need to have access to the actual exception info you may use: the actual exception raised. The main attributes of interest are ``.type``, ``.value`` and ``.traceback``. +Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement). +If you want to check if a block of code is raising an exact exception type, you need to check that explicitly: + + +.. code-block:: python + + def test_foo_not_implemented(): + def foo(): + raise NotImplementedError + + with pytest.raises(RuntimeError) as excinfo: + foo() + assert excinfo.type is RuntimeError + +The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because +:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will +catch the problem. + +Matching exception messages +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + You can pass a ``match`` keyword parameter to the context-manager to test that a regular expression matches on the string representation of an exception (similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``): @@ -116,36 +136,113 @@ that a regular expression matches on the string representation of an exception with pytest.raises(ValueError, match=r".* 123 .*"): myfunc() -The regexp parameter of the ``match`` method is matched with the ``re.search`` -function, so in the above example ``match='123'`` would have worked as -well. +Notes: + +* The ``match`` parameter is matched with the :func:`re.search` + function, so in the above example ``match='123'`` would have worked as well. +* The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``. + + +.. _`assert-matching-exception-groups`: -There's an alternate form of the :func:`pytest.raises` function where you pass -a function that will be executed with the given ``*args`` and ``**kwargs`` and -assert that the given exception is raised: +Matching exception groups +~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>` +method to test for exceptions returned as part of an :class:`ExceptionGroup`: .. code-block:: python - pytest.raises(ExpectedException, func, *args, **kwargs) + def test_exception_in_group(): + with pytest.raises(ExceptionGroup) as excinfo: + raise ExceptionGroup( + "Group message", + [ + RuntimeError("Exception 123 raised"), + ], + ) + assert excinfo.group_contains(RuntimeError, match=r".* 123 .*") + assert not excinfo.group_contains(TypeError) + +The optional ``match`` keyword parameter works the same way as for +:func:`pytest.raises`. + +By default ``group_contains()`` will recursively search for a matching +exception at any level of nested ``ExceptionGroup`` instances. You can +specify a ``depth`` keyword parameter if you only want to match an +exception at a specific level; exceptions contained directly in the top +``ExceptionGroup`` would match ``depth=1``. + +.. code-block:: python + + def test_exception_in_group_at_given_depth(): + with pytest.raises(ExceptionGroup) as excinfo: + raise ExceptionGroup( + "Group message", + [ + RuntimeError(), + ExceptionGroup( + "Nested group", + [ + TypeError(), + ], + ), + ], + ) + assert excinfo.group_contains(RuntimeError, depth=1) + assert excinfo.group_contains(TypeError, depth=2) + assert not excinfo.group_contains(RuntimeError, depth=2) + assert not excinfo.group_contains(TypeError, depth=1) + +Alternate form (legacy) +~~~~~~~~~~~~~~~~~~~~~~~ + +There is an alternate form where you pass +a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises` +will execute the function with the arguments and assert that the given exception is raised: + +.. code-block:: python + + def func(x): + if x <= 0: + raise ValueError("x needs to be larger than zero") + + + pytest.raises(ValueError, func, x=-1) The reporter will provide you with helpful output in case of failures such as *no exception* or *wrong exception*. -Note that it is also possible to specify a "raises" argument to -``pytest.mark.xfail``, which checks that the test is failing in a more +This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was +added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``) +being considered more readable. +Nonetheless, this form is fully supported and not deprecated in any way. + +xfail mark and pytest.raises +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is also possible to specify a ``raises`` argument to +:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more specific way than just having any exception raised: .. code-block:: python + def f(): + raise IndexError() + + @pytest.mark.xfail(raises=IndexError) def test_f(): f() -Using :func:`pytest.raises` is likely to be better for cases where you are -testing exceptions your own code is deliberately raising, whereas using -``@pytest.mark.xfail`` with a check function is probably better for something -like documenting unfixed bugs (where the test describes what "should" happen) -or bugs in dependencies. + +This will only "xfail" if the test fails by raising ``IndexError`` or subclasses. + +* Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something + like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies. + +* Using :func:`pytest.raises` is likely to be better for cases where you are + testing exceptions your own code is deliberately raising, which is the majority of cases. .. _`assertwarns`: @@ -183,7 +280,7 @@ if you run this module: $ pytest test_assert2.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -197,11 +294,12 @@ if you run this module: set2 = set("8035") > assert set1 == set2 E AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'} + E E Extra items in the left set: E '1' E Extra items in the right set: E '5' - E Use -v to get the full diff + E Use -v to get more diff test_assert2.py:4: AssertionError ========================= short test summary info ========================== @@ -238,7 +336,7 @@ file which provides an alternative explanation for ``Foo`` objects: if isinstance(left, Foo) and isinstance(right, Foo) and op == "==": return [ "Comparing Foo instances:", - " vals: {} != {}".format(left.val, right.val), + f" vals: {left.val} != {right.val}", ] now, given this test module: diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst index 245dfd6d9a8..117ff7ec13b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/bash-completion.rst @@ -5,7 +5,7 @@ How to set up bash completion ============================= When using bash as your shell, ``pytest`` can use argcomplete -(https://argcomplete.readthedocs.io/) for auto-completion. +(https://kislyuk.github.io/argcomplete/) for auto-completion. For this ``argcomplete`` needs to be installed **and** enabled. Install argcomplete using: diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/cache.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/cache.rst index e7994645dd3..40cd3f00dd6 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/cache.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/cache.rst @@ -86,7 +86,7 @@ If you then run it with ``--lf``: $ pytest --lf =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items run-last-failure: rerun previous 2 failures @@ -132,7 +132,7 @@ of ``FF`` and dots): $ pytest --ff =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 50 items run-last-failure: rerun previous 2 failures first @@ -176,14 +176,21 @@ with more recent files coming first. Behavior when no tests failed in the last run --------------------------------------------- -When no tests failed in the last run, or when no cached ``lastfailed`` data was -found, ``pytest`` can be configured either to run all of the tests or no tests, -using the ``--last-failed-no-failures`` option, which takes one of the following values: +The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``. +Determines whether to execute tests when there are no previously (known) +failures or when no cached ``lastfailed`` data was found. + +There are two options: + +* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default. +* ``none``: when there are no known test failures, just emits a message stating this and exit successfully. + +Example: .. code-block:: bash - pytest --last-failed --last-failed-no-failures all # run all tests (default behavior) - pytest --last-failed --last-failed-no-failures none # run no tests and exit + pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior) + pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully The new config.cache object -------------------------------- @@ -199,7 +206,6 @@ across pytest invocations: # content of test_caching.py import pytest - import time def expensive_computation(): @@ -207,12 +213,12 @@ across pytest invocations: @pytest.fixture - def mydata(request): - val = request.config.cache.get("example/value", None) + def mydata(pytestconfig): + val = pytestconfig.cache.get("example/value", None) if val is None: expensive_computation() val = 42 - request.config.cache.set("example/value", val) + pytestconfig.cache.set("example/value", val) return val @@ -234,7 +240,7 @@ If you run this command for the first time, you can see the print statement: > assert mydata == 23 E assert 42 == 23 - test_caching.py:20: AssertionError + test_caching.py:19: AssertionError -------------------------- Captured stdout setup --------------------------- running expensive computation... ========================= short test summary info ========================== @@ -257,7 +263,7 @@ the cache and nothing will be printed: > assert mydata == 23 E assert 42 == 23 - test_caching.py:20: AssertionError + test_caching.py:19: AssertionError ========================= short test summary info ========================== FAILED test_caching.py::test_function - assert 42 == 23 1 failed in 0.12s @@ -275,7 +281,7 @@ You can always peek at the content of the cache using the $ pytest --cache-show =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project cachedir: /home/sweet/project/.pytest_cache --------------------------- cache values for '*' --------------------------- @@ -297,7 +303,7 @@ filtering: $ pytest --cache-show example/* =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project cachedir: /home/sweet/project/.pytest_cache ----------------------- cache values for 'example/*' ----------------------- diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst index 9ccea719b64..5e23f0c024e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-stdout-stderr.rst @@ -83,7 +83,7 @@ of the failing function and hide the other one: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst index 065c11e610c..afabad5da14 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/capture-warnings.rst @@ -28,7 +28,7 @@ Running pytest now produces this output: $ pytest test_show_warnings.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -42,6 +42,8 @@ Running pytest now produces this output: -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ======================= 1 passed, 1 warning in 0.12s ======================= +.. _`controlling-warnings`: + Controlling warnings -------------------- @@ -107,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last is performed. +.. note:: + + The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are + similar in structure, but each configuration option interprets its filter + differently. For example, *message* in ``filterwarnings`` is a string containing a + regular expression that the start of the warning message must match, + case-insensitively, while *message* in ``-W`` is a literal string that the start of + the warning message must contain (case-insensitively), ignoring any whitespace at + the start or end of message. Consult the `warning filter`_ documentation for more + details. + + .. _`filterwarnings`: ``@pytest.mark.filterwarnings`` @@ -176,11 +190,14 @@ using an external system. DeprecationWarning and PendingDeprecationWarning ------------------------------------------------ - By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings from user code and third-party libraries, as recommended by :pep:`565`. This helps users keep their code modern and avoid breakages when deprecated warnings are effectively removed. +However, in the specific case where users capture any type of warnings in their test, either with +:func:`pytest.warns`, :func:`pytest.deprecated_call` or using the :ref:`recwarn <recwarn>` fixture, +no warning will be displayed at all. + Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over (such as third-party libraries), in which case you might use the warning filters options (ini or marks) to ignore those warnings. @@ -197,6 +214,9 @@ For example: This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches the regular expression ``".*U.*mode is deprecated"``. +See :ref:`@pytest.mark.filterwarnings <filterwarnings>` and +:ref:`Controlling warnings <controlling-warnings>` for more examples. + .. note:: If warnings are configured at the interpreter level, using @@ -245,14 +265,15 @@ when called with a ``17`` argument. Asserting warnings with the warns function ------------------------------------------ - - You can check that code raises a particular warning using :func:`pytest.warns`, -which works in a similar manner to :ref:`raises <assertraises>`: +which works in a similar manner to :ref:`raises <assertraises>` (except that +:ref:`raises <assertraises>` does not capture all exceptions, only the +``expected_exception``): .. code-block:: python import warnings + import pytest @@ -260,21 +281,35 @@ which works in a similar manner to :ref:`raises <assertraises>`: with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) -The test will fail if the warning in question is not raised. The keyword -argument ``match`` to assert that the exception matches a text or regex:: +The test will fail if the warning in question is not raised. Use the keyword +argument ``match`` to assert that the warning matches a text or regex. +To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can +first be escaped with ``re.escape``. + +Some examples: - >>> with warns(UserWarning, match='must be 0 or None'): +.. code-block:: pycon + + + >>> with warns(UserWarning, match="must be 0 or None"): ... warnings.warn("value must be 0 or None", UserWarning) + ... - >>> with warns(UserWarning, match=r'must be \d+$'): + >>> with warns(UserWarning, match=r"must be \d+$"): ... warnings.warn("value must be 42", UserWarning) + ... - >>> with warns(UserWarning, match=r'must be \d+$'): + >>> with warns(UserWarning, match=r"must be \d+$"): ... warnings.warn("this is not here", UserWarning) + ... Traceback (most recent call last): ... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... + >>> with warns(UserWarning, match=re.escape("issue with foo() func")): + ... warnings.warn("issue with foo() func") + ... + You can also call :func:`pytest.warns` on a function or code string: .. code-block:: python @@ -347,8 +382,6 @@ warnings: a WarningsRecorder instance. To view the recorded warnings, you can iterate over this instance, call ``len`` on it to get the number of recorded warnings, or index into it to get a particular recorded warning. -.. currentmodule:: _pytest.warnings - Full API: :class:`~_pytest.recwarn.WarningsRecorder`. .. _`warns use cases`: @@ -358,20 +391,32 @@ Additional use cases of warnings in tests Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them: -- To ensure that **any** warning is emitted, use: +- To ensure that **at least one** of the indicated warnings is issued, use: .. code-block:: python - with pytest.warns(): + def test_warning(): + with pytest.warns((RuntimeWarning, UserWarning)): + ... + +- To ensure that **only** certain warnings are issued, use: + +.. code-block:: python + + def test_warning(recwarn): ... + assert len(recwarn) == 1 + user_warning = recwarn.pop(UserWarning) + assert issubclass(user_warning.category, UserWarning) - To ensure that **no** warnings are emitted, use: .. code-block:: python - with warnings.catch_warnings(): - warnings.simplefilter("error") - ... + def test_warning(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + ... - To suppress warnings, use: @@ -441,3 +486,18 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep features. The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`. + + +.. _`resource-warnings`: + +Resource Warnings +----------------- + +Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if +:mod:`tracemalloc` module is enabled. + +One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large +enough number of frames (say ``20``, but that number is application dependent). + +For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__ +section in the Python documentation. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst index ce0b5a5f649..c2a6cc8e958 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/doctest.rst @@ -30,7 +30,7 @@ then you can just invoke ``pytest`` directly: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -58,7 +58,7 @@ and functions, including from test modules: $ pytest --doctest-modules =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -126,14 +126,17 @@ pytest also introduces new options: in expected doctest output. * ``NUMBER``: when enabled, floating-point numbers only need to match as far as - the precision you have written in the expected doctest output. For example, - the following output would only need to match to 2 decimal places:: + the precision you have written in the expected doctest output. The numbers are + compared using :func:`pytest.approx` with relative tolerance equal to the + precision. For example, the following output would only need to match to 2 + decimal places when comparing ``3.14`` to + ``pytest.approx(math.pi, rel=10**-2)``:: >>> math.pi 3.14 - If you wrote ``3.1416`` then the actual output would need to match to 4 - decimal places; and so on. + If you wrote ``3.1416`` then the actual output would need to match to + approximately 4 decimal places; and so on. This avoids false positives caused by limited floating-point precision, like this:: @@ -221,6 +224,7 @@ place the objects you want to appear in the doctest namespace: .. code-block:: python # content of conftest.py + import pytest import numpy @@ -239,7 +243,6 @@ which can then be used in your doctests directly: >>> len(a) 10 """ - pass Note that like the normal ``conftest.py``, the fixtures are discovered in the directory tree conftest is in. Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst index 9909e7d113a..1c37023c72a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/existingtestsuite.rst @@ -4,8 +4,8 @@ How to use pytest with an existing test suite ============================================== Pytest can be used with most existing test suites, but its -behavior differs from other test runners such as :ref:`nose <noseintegration>` or -Python's default unittest framework. +behavior differs from other test runners such as Python's +default unittest framework. Before using this section you will want to :ref:`install pytest <getstarted>`. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/failures.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/failures.rst index ef87550915a..b3d0c155b48 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/failures.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/failures.rst @@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions .. versionadded:: 6.2 -.. note:: - - These features only work on Python>=3.8. - Unhandled exceptions are exceptions that are raised in a situation in which they cannot propagate to a caller. The most common case is an exception raised in a :meth:`__del__ <object.__del__>` implementation. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst index 08013877455..6cc20c8c3e4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/fixtures.rst @@ -398,9 +398,10 @@ access the fixture function: .. code-block:: python # content of conftest.py - import pytest import smtplib + import pytest + @pytest.fixture(scope="module") def smtp_connection(): @@ -432,7 +433,7 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: $ pytest test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -493,7 +494,7 @@ Fixtures are created when first requested by a test, and are destroyed based on * ``function``: the default scope, the fixture is destroyed at the end of the test. * ``class``: the fixture is destroyed during teardown of the last test in the class. * ``module``: the fixture is destroyed during teardown of the last test in the module. -* ``package``: the fixture is destroyed during teardown of the last test in the package. +* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it. * ``session``: the fixture is destroyed at the end of the test session. .. note:: @@ -609,10 +610,10 @@ Here's what that might look like: .. code-block:: python # content of test_emaillib.py - import pytest - from emaillib import Email, MailAdminClient + import pytest + @pytest.fixture def mail_admin(): @@ -630,6 +631,7 @@ Here's what that might look like: def receiving_user(mail_admin): user = mail_admin.create_user() yield user + user.clear_mailbox() mail_admin.delete_user(user) @@ -683,10 +685,10 @@ Here's how the previous example would look using the ``addfinalizer`` method: .. code-block:: python # content of test_emaillib.py - import pytest - from emaillib import Email, MailAdminClient + import pytest + @pytest.fixture def mail_admin(): @@ -736,6 +738,87 @@ does offer some nuances for when you're in a pinch. . [100%] 1 passed in 0.12s +Note on finalizer order +"""""""""""""""""""""""" + +Finalizers are executed in a first-in-last-out order. +For yield fixtures, the first teardown code to run is from the right-most fixture, i.e. the last test parameter. + + +.. code-block:: python + + # content of test_finalizers.py + import pytest + + + def test_bar(fix_w_yield1, fix_w_yield2): + print("test_bar") + + + @pytest.fixture + def fix_w_yield1(): + yield + print("after_yield_1") + + + @pytest.fixture + def fix_w_yield2(): + yield + print("after_yield_2") + + +.. code-block:: pytest + + $ pytest -s test_finalizers.py + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + collected 1 item + + test_finalizers.py test_bar + .after_yield_2 + after_yield_1 + + + ============================ 1 passed in 0.12s ============================= + +For finalizers, the first fixture to run is last call to `request.addfinalizer`. + +.. code-block:: python + + # content of test_finalizers.py + from functools import partial + import pytest + + + @pytest.fixture + def fix_w_finalizers(request): + request.addfinalizer(partial(print, "finalizer_2")) + request.addfinalizer(partial(print, "finalizer_1")) + + + def test_bar(fix_w_finalizers): + print("test_bar") + + +.. code-block:: pytest + + $ pytest -s test_finalizers.py + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + collected 1 item + + test_finalizers.py test_bar + .finalizer_1 + finalizer_2 + + + ============================ 1 passed in 0.12s ============================= + +This is so because yield fixtures use `addfinalizer` behind the scenes: when the fixture executes, `addfinalizer` registers a function that resumes the generator, which in turn calls the teardown code. + + .. _`safe teardowns`: Safe teardowns @@ -752,10 +835,10 @@ above): .. code-block:: python # content of test_emaillib.py - import pytest - from emaillib import Email, MailAdminClient + import pytest + @pytest.fixture def setup(): @@ -1030,16 +1113,17 @@ read an optional server URL from the test module which uses our fixture: .. code-block:: python # content of conftest.py - import pytest import smtplib + import pytest + @pytest.fixture(scope="module") def smtp_connection(request): server = getattr(request.module, "smtpserver", "smtp.gmail.com") smtp_connection = smtplib.SMTP(server, 587, timeout=5) yield smtp_connection - print("finalizing {} ({})".format(smtp_connection, server)) + print(f"finalizing {smtp_connection} ({server})") smtp_connection.close() We use the ``request.module`` attribute to optionally obtain an @@ -1153,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care @pytest.fixture def make_customer_record(): - created_records = [] def _make_customer_record(name): @@ -1188,20 +1271,21 @@ configured in multiple ways. Extending the previous example, we can flag the fixture to create two ``smtp_connection`` fixture instances which will cause all tests using the fixture to run twice. The fixture function gets access to each parameter -through the special :py:class:`request <FixtureRequest>` object: +through the special :py:class:`request <pytest.FixtureRequest>` object: .. code-block:: python # content of conftest.py - import pytest import smtplib + import pytest + @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection - print("finalizing {}".format(smtp_connection)) + print(f"finalizing {smtp_connection}") smtp_connection.close() The main change is the declaration of ``params`` with @@ -1330,27 +1414,30 @@ Running the above tests results in the following test IDs being used: $ pytest --collect-only =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project - collected 11 items - - <Module test_anothersmtp.py> - <Function test_showhelo[smtp.gmail.com]> - <Function test_showhelo[mail.python.org]> - <Module test_emaillib.py> - <Function test_email_received> - <Module test_ids.py> - <Function test_a[spam]> - <Function test_a[ham]> - <Function test_b[eggs]> - <Function test_b[1]> - <Module test_module.py> - <Function test_ehlo[smtp.gmail.com]> - <Function test_noop[smtp.gmail.com]> - <Function test_ehlo[mail.python.org]> - <Function test_noop[mail.python.org]> - - ======================= 11 tests collected in 0.12s ======================== + collected 12 items + + <Dir fixtures.rst-218> + <Module test_anothersmtp.py> + <Function test_showhelo[smtp.gmail.com]> + <Function test_showhelo[mail.python.org]> + <Module test_emaillib.py> + <Function test_email_received> + <Module test_finalizers.py> + <Function test_bar> + <Module test_ids.py> + <Function test_a[spam]> + <Function test_a[ham]> + <Function test_b[eggs]> + <Function test_b[1]> + <Module test_module.py> + <Function test_ehlo[smtp.gmail.com]> + <Function test_noop[smtp.gmail.com]> + <Function test_ehlo[mail.python.org]> + <Function test_noop[mail.python.org]> + + ======================= 12 tests collected in 0.12s ======================== .. _`fixture-parametrize-marks`: @@ -1382,7 +1469,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 3 items @@ -1432,7 +1519,7 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 2 items @@ -1503,7 +1590,7 @@ to show the setup/teardown flow: def test_2(otherarg, modarg): - print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg)) + print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}") Let's run the tests in verbose mode and with looking at the print-output: @@ -1512,7 +1599,7 @@ Let's run the tests in verbose mode and with looking at the print-output: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: /home/sweet/project collecting ... collected 8 items @@ -1604,6 +1691,7 @@ and declare its use in a test module via a ``usefixtures`` marker: # content of test_setenv.py import os + import pytest @@ -1611,7 +1699,7 @@ and declare its use in a test module via a ``usefixtures`` marker: class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] - with open("myfile", "w") as f: + with open("myfile", "w", encoding="utf-8") as f: f.write("hello") def test_cwd_again_starts_empty(self): @@ -1633,8 +1721,7 @@ You can specify multiple fixtures like this: .. code-block:: python @pytest.mark.usefixtures("cleandir", "anotherfixture") - def test(): - ... + def test(): ... and you may specify fixture usage at the test module level using :globalvar:`pytestmark`: @@ -1662,11 +1749,9 @@ into an ini-file: @pytest.mark.usefixtures("my_other_fixture") @pytest.fixture - def my_fixture_that_sadly_wont_use_my_other_fixture(): - ... + def my_fixture_that_sadly_wont_use_my_other_fixture(): ... - Currently this will not generate any error or warning, but this is intended - to be handled by :issue:`3664`. + This generates a deprecation warning, and will become an error in Pytest 8. .. _`override fixtures`: @@ -1684,8 +1769,6 @@ Given the tests file structure is: :: tests/ - __init__.py - conftest.py # content of tests/conftest.py import pytest @@ -1700,8 +1783,6 @@ Given the tests file structure is: assert username == 'username' subfolder/ - __init__.py - conftest.py # content of tests/subfolder/conftest.py import pytest @@ -1710,8 +1791,8 @@ Given the tests file structure is: def username(username): return 'overridden-' + username - test_something.py - # content of tests/subfolder/test_something.py + test_something_else.py + # content of tests/subfolder/test_something_else.py def test_username(username): assert username == 'overridden-username' @@ -1727,8 +1808,6 @@ Given the tests file structure is: :: tests/ - __init__.py - conftest.py # content of tests/conftest.py import pytest @@ -1770,8 +1849,6 @@ Given the tests file structure is: :: tests/ - __init__.py - conftest.py # content of tests/conftest.py import pytest @@ -1808,8 +1885,6 @@ Given the tests file structure is: :: tests/ - __init__.py - conftest.py # content of tests/conftest.py import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/index.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/index.rst index 6f52aaecdc3..225f289651e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/index.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/index.rst @@ -52,7 +52,6 @@ pytest and other test systems existingtestsuite unittest - nose xunit_setup pytest development environment diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/logging.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/logging.rst index 2e8734fa6a3..300e9f6e6c2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/logging.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/logging.rst @@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file: log_format = %(asctime)s %(levelname)s %(message)s log_date_format = %Y-%m-%d %H:%M:%S +Specific loggers can be disabled via ``--log-disable={logger_name}``. +This argument can be passed multiple times: + +.. code-block:: bash + + pytest --log-disable=main --log-disable=testing + Further it is possible to disable reporting of captured content (stdout, stderr and logs) on failed tests completely with: @@ -73,7 +80,6 @@ messages. This is supported by the ``caplog`` fixture: def test_foo(caplog): caplog.set_level(logging.INFO) - pass By default the level is set on the root logger, however as a convenience it is also possible to set the log level of any @@ -83,7 +89,6 @@ logger: def test_foo(caplog): caplog.set_level(logging.CRITICAL, logger="root.baz") - pass The log levels set are restored automatically at the end of the test. @@ -161,14 +166,19 @@ the records for the ``setup`` and ``call`` stages during teardown like so: x.message for x in caplog.get_records(when) if x.levelno == logging.WARNING ] if messages: - pytest.fail( - "warning messages encountered during testing: {}".format(messages) - ) + pytest.fail(f"warning messages encountered during testing: {messages}") The full API is available at :class:`pytest.LogCaptureFixture`. +.. warning:: + + The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is + modified during a test, for example with ``logging.config.dictConfig``, this handler may be + removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration + only adds to the existing handlers. + .. _live_logs: @@ -180,8 +190,8 @@ logging records as they are emitted directly into the console. You can specify the logging level for which log records with equal or higher level are printed to the console by passing ``--log-cli-level``. This setting -accepts the logging level names as seen in python's documentation or an integer -as the logging level num. +accepts the logging level names or numeric values as seen in +:ref:`logging's documentation <python:levels>`. Additionally, you can also specify ``--log-cli-format`` and ``--log-cli-date-format`` which mirror and default to ``--log-format`` and @@ -196,13 +206,15 @@ option names are: * ``log_cli_date_format`` If you need to record the whole test suite logging calls to a file, you can pass -``--log-file=/path/to/log/file``. This log file is opened in write mode which +``--log-file=/path/to/log/file``. This log file is opened in write mode by default which means that it will be overwritten at each run tests session. +If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``. +Note that relative paths for the log-file location, whether passed on the CLI or declared in a +config file, are always resolved relative to the current working directory. You can also specify the logging level for the log file by passing -``--log-file-level``. This setting accepts the logging level names as seen in -python's documentation(ie, uppercased level names) or an integer as the logging -level num. +``--log-file-level``. This setting accepts the logging level names or numeric +values as seen in :ref:`logging's documentation <python:levels>`. Additionally, you can also specify ``--log-file-format`` and ``--log-file-date-format`` which are equal to ``--log-format`` and @@ -212,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The option names are: * ``log_file`` +* ``log_file_mode`` * ``log_file_level`` * ``log_file_format`` * ``log_file_date_format`` You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality -is considered **experimental**. +is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option. .. _log_colors: @@ -230,7 +243,7 @@ through ``add_color_level()``. Example: .. code-block:: python - @pytest.hookimpl + @pytest.hookimpl(trylast=True) def pytest_configure(config): logging_plugin = config.pluginmanager.get_plugin("logging-plugin") diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst index 9c61233f7e5..a9504dcb32a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/monkeypatch.rst @@ -3,7 +3,7 @@ How to monkeypatch/mock modules and environments ================================================================ -.. currentmodule:: _pytest.monkeypatch +.. currentmodule:: pytest Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily @@ -14,17 +14,16 @@ environment variable, or to modify ``sys.path`` for importing. The ``monkeypatch`` fixture provides these helper methods for safely patching and mocking functionality in tests: -.. code-block:: python +* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>` +* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>` +* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>` +* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>` +* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>` +* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>` +* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>` +* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>` +* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>` - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.setattr("somemodule.obj.name", value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) All modifications will be undone after the requesting test function or fixture has finished. The ``raising`` @@ -55,13 +54,16 @@ during a test. 5. Use :py:meth:`monkeypatch.syspath_prepend <MonkeyPatch.syspath_prepend>` to modify ``sys.path`` which will also call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`. +6. Use :py:meth:`monkeypatch.context <MonkeyPatch.context>` to apply patches only in a specific scope, which can help +control teardown of complex fixtures or patches to the stdlib. + See the `monkeypatch blog post`_ for some introduction material and a discussion of its motivation. .. _`monkeypatch blog post`: https://tetamap.wordpress.com//2009/03/03/monkeypatching-in-unit-tests-done-right/ -Simple example: monkeypatching functions ----------------------------------------- +Monkeypatching functions +------------------------ Consider a scenario where you are working with user directories. In the context of testing, you do not want your test to depend on the running user. ``monkeypatch`` @@ -133,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``. # this is the previous code block example import app + # custom class to be the mock return value # will override the requests.Response returned from requests.get class MockResponse: - # mock json() method always returns a specific testing dictionary @staticmethod def json(): @@ -144,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``. def test_get_json(monkeypatch): - # Any arguments may be passed and mock_get() will always return our # mocked object, which only has the .json() method. def mock_get(*args, **kwargs): @@ -179,6 +180,7 @@ This mock can be shared across tests using a ``fixture``: # app.py that includes the get_json() function import app + # custom class to be the mock return value of requests.get() class MockResponse: @staticmethod @@ -356,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific def test_connection(monkeypatch): - # Patch the values of DEFAULT_CONFIG to specific # testing values only for this test. monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user") @@ -381,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v def test_missing_user(monkeypatch): - # patch the DEFAULT_CONFIG t be missing the 'user' key monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False) @@ -402,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests # app.py with the connection string function import app + # all of the mocks are moved into separated fixtures @pytest.fixture def mock_test_user(monkeypatch): @@ -423,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests # tests reference only the fixture mocks that are needed def test_connection(mock_test_user, mock_test_database): - expected = "User Id=test_user; Location=test_db;" result = app.create_connection_string() @@ -431,12 +431,11 @@ separate fixtures for each potential mock and reference them in the needed tests def test_missing_user(mock_missing_default_user): - with pytest.raises(KeyError): _ = app.create_connection_string() -.. currentmodule:: _pytest.monkeypatch +.. currentmodule:: pytest API Reference ------------- diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/nose.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/nose.rst deleted file mode 100644 index 4bf8b06c324..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/nose.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. _`noseintegration`: - -How to run tests written for nose -======================================= - -``pytest`` has basic support for running tests written for nose_. - -.. _nosestyle: - -Usage -------------- - -After :ref:`installation` type: - -.. code-block:: bash - - python setup.py develop # make sure tests can import our package - pytest # instead of 'nosetests' - -and you should be able to run your nose style tests and -make use of pytest's capabilities. - -Supported nose Idioms ----------------------- - -* setup and teardown at module/class/method level -* SkipTest exceptions and markers -* setup/teardown decorators -* ``__test__`` attribute on modules/classes/functions -* general usage of nose utilities - -Unsupported idioms / known issues ----------------------------------- - -- unittest-style ``setUp, tearDown, setUpClass, tearDownClass`` - are recognized only on ``unittest.TestCase`` classes but not - on plain classes. ``nose`` supports these methods also on plain - classes but pytest deliberately does not. As nose and pytest already - both support ``setup_class, teardown_class, setup_method, teardown_method`` - it doesn't seem useful to duplicate the unittest-API like nose does. - If you however rather think pytest should support the unittest-spelling on - plain classes please post to :issue:`377`. - -- nose imports test modules with the same import path (e.g. - ``tests.test_mode``) but different file system paths - (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) - by extending sys.path/import semantics. pytest does not do that - but there is discussion in :issue:`268` for adding some support. Note that - `nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_. - - If you place a conftest.py file in the root directory of your project - (as determined by pytest) pytest will run tests "nose style" against - the code below that directory by adding it to your ``sys.path`` instead of - running against your installed code. - - You may find yourself wanting to do this if you ran ``python setup.py install`` - to set up your project, as opposed to ``python setup.py develop`` or any of - the package manager equivalents. Installing with develop in a - virtual environment like tox is recommended over this pattern. - -- nose-style doctests are not collected and executed correctly, - also doctest fixtures don't work. - -- no nose-configuration is recognized. - -- ``yield``-based methods are unsupported as of pytest 4.1.0. They are - fundamentally incompatible with pytest because they don't support fixtures - properly since collection and test execution are separated. - -Migrating from nose to pytest ------------------------------- - -`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script -and pytest plugin to help convert Nose-based tests into pytest-based tests. -Specifically, the script transforms nose.tools.assert_* function calls into -raw assert statements, while preserving format of original arguments -as much as possible. - -.. _nose: https://nose.readthedocs.io/en/latest/ diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/output.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/output.rst index 4b90988f49d..7a4e32edc78 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/output.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/output.rst @@ -12,8 +12,15 @@ Examples for modifying traceback printing: .. code-block:: bash - pytest --showlocals # show local variables in tracebacks - pytest -l # show local variables (shortcut) + pytest --showlocals # show local variables in tracebacks + pytest -l # show local variables (shortcut) + pytest --no-showlocals # hide local variables (if addopts enables them) + + pytest --capture=fd # default, capture at the file descriptor level + pytest --capture=sys # capture at the sys level + pytest --capture=no # don't capture + pytest -s # don't capture (shortcut) + pytest --capture=tee-sys # capture to logs but also output to sys level streams pytest --tb=auto # (default) 'long' tracebacks for the first and last # entry, but 'short' style for the other entries @@ -35,6 +42,16 @@ option you make sure a trace is shown. Verbosity -------------------------------------------------- +Examples for modifying printing verbosity: + +.. code-block:: bash + + pytest --quiet # quiet - less verbose - mode + pytest -q # quiet - less verbose - mode (shortcut) + pytest -v # increase verbosity, display individual test names + pytest -vv # more verbose, display more details from the test output + pytest -vvv # not a standard , but may be used for even more detail in certain setups + The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion details when tests fail, fixtures details with ``--fixtures``, etc. @@ -83,8 +100,9 @@ Executing pytest normally gives us this output (we are skipping the header to fo fruits2 = ["banana", "apple", "orange", "melon", "kiwi"] > assert fruits1 == fruits2 E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi'] + E E At index 2 diff: 'grapes' != 'orange' - E Use -v to get the full diff + E Use -v to get more diff test_verbosity_example.py:8: AssertionError ____________________________ test_numbers_fail _____________________________ @@ -94,12 +112,13 @@ Executing pytest normally gives us this output (we are skipping the header to fo number_to_text2 = {str(x * 10): x * 10 for x in range(5)} > assert number_to_text1 == number_to_text2 E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...} + E E Omitting 1 identical items, use -vv to show E Left contains 4 more items: E {'1': 1, '2': 2, '3': 3, '4': 4} E Right contains 4 more items: E {'10': 10, '20': 20, '30': 30, '40': 40} - E Use -v to get the full diff + E Use -v to get more diff test_verbosity_example.py:14: AssertionError ___________________________ test_long_text_fail ____________________________ @@ -145,12 +164,15 @@ Now we can increase pytest's verbosity: fruits2 = ["banana", "apple", "orange", "melon", "kiwi"] > assert fruits1 == fruits2 E AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi'] + E E At index 2 diff: 'grapes' != 'orange' + E E Full diff: - E - ['banana', 'apple', 'orange', 'melon', 'kiwi'] - E ? ^ ^^ - E + ['banana', 'apple', 'grapes', 'melon', 'kiwi'] - E ? ^ ^ + + E [ + E 'banana', + E 'apple',... + E + E ...Full output truncated (7 lines hidden), use '-vv' to show test_verbosity_example.py:8: AssertionError ____________________________ test_numbers_fail _____________________________ @@ -160,15 +182,15 @@ Now we can increase pytest's verbosity: number_to_text2 = {str(x * 10): x * 10 for x in range(5)} > assert number_to_text1 == number_to_text2 E AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...} + E E Omitting 1 identical items, use -vv to show E Left contains 4 more items: E {'1': 1, '2': 2, '3': 3, '4': 4} E Right contains 4 more items: E {'10': 10, '20': 20, '30': 30, '40': 40} - E Full diff: - E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}... + E ... E - E ...Full output truncated (3 lines hidden), use '-vv' to show + E ...Full output truncated (16 lines hidden), use '-vv' to show test_verbosity_example.py:14: AssertionError ___________________________ test_long_text_fail ____________________________ @@ -214,12 +236,20 @@ Now if we increase verbosity even more: fruits2 = ["banana", "apple", "orange", "melon", "kiwi"] > assert fruits1 == fruits2 E AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi'] + E E At index 2 diff: 'grapes' != 'orange' + E E Full diff: - E - ['banana', 'apple', 'orange', 'melon', 'kiwi'] - E ? ^ ^^ - E + ['banana', 'apple', 'grapes', 'melon', 'kiwi'] - E ? ^ ^ + + E [ + E 'banana', + E 'apple', + E - 'orange', + E ? ^ ^^ + E + 'grapes', + E ? ^ ^ + + E 'melon', + E 'kiwi', + E ] test_verbosity_example.py:8: AssertionError ____________________________ test_numbers_fail _____________________________ @@ -229,16 +259,30 @@ Now if we increase verbosity even more: number_to_text2 = {str(x * 10): x * 10 for x in range(5)} > assert number_to_text1 == number_to_text2 E AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40} + E E Common items: E {'0': 0} E Left contains 4 more items: E {'1': 1, '2': 2, '3': 3, '4': 4} E Right contains 4 more items: E {'10': 10, '20': 20, '30': 30, '40': 40} + E E Full diff: - E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40} - E ? - - - - - - - - - E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} + E { + E '0': 0, + E - '10': 10, + E ? - - + E + '1': 1, + E - '20': 20, + E ? - - + E + '2': 2, + E - '30': 30, + E ? - - + E + '3': 3, + E - '40': 40, + E ? - - + E + '4': 4, + E } test_verbosity_example.py:14: AssertionError ___________________________ test_long_text_fail ____________________________ @@ -250,9 +294,47 @@ Now if we increase verbosity even more: test_verbosity_example.py:19: AssertionError ========================= short test summary info ========================== - FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser... - FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass... - FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a... + FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi'] + + At index 2 diff: 'grapes' != 'orange' + + Full diff: + [ + 'banana', + 'apple', + - 'orange', + ? ^ ^^ + + 'grapes', + ? ^ ^ + + 'melon', + 'kiwi', + ] + FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40} + + Common items: + {'0': 0} + Left contains 4 more items: + {'1': 1, '2': 2, '3': 3, '4': 4} + Right contains 4 more items: + {'10': 10, '20': 20, '30': 30, '40': 40} + + Full diff: + { + '0': 0, + - '10': 10, + ? - - + + '1': 1, + - '20': 20, + ? - - + + '2': 2, + - '30': 30, + ? - - + + '3': 3, + - '40': 40, + ? - - + + '4': 4, + } + FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet ' ======================= 3 failed, 1 passed in 0.12s ======================== Notice now that: @@ -269,6 +351,22 @@ situations, for example you are shown even fixtures that start with ``_`` if you Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment, however some plugins might make use of higher verbosity. +.. _`pytest.fine_grained_verbosity`: + +Fine-grained verbosity +~~~~~~~~~~~~~~~~~~~~~~ + +In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently. +This is done by setting a verbosity level in the configuration file for the specific aspect of the output. + +:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running +``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside +the file is shown by a single character in the output. + +:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed. +Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each +test inside the file gets its own line in the output. + .. _`pytest.detailed_failed_tests_usage`: Producing a detailed summary report @@ -323,7 +421,7 @@ Example: $ pytest -ra =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 6 items @@ -346,11 +444,19 @@ Example: E assert 0 test_example.py:14: AssertionError + ================================ XFAILURES ================================= + ________________________________ test_xfail ________________________________ + + def test_xfail(): + > pytest.xfail("xfailing this test") + E _pytest.outcomes.XFailed: xfailing this test + + test_example.py:26: XFailed + ================================= XPASSES ================================== ========================= short test summary info ========================== SKIPPED [1] test_example.py:22: skipping this test - XFAIL test_example.py::test_xfail - reason: xfailing this test - XPASS test_example.py::test_xpass always xfail + XFAIL test_example.py::test_xfail - reason: xfailing this test + XPASS test_example.py::test_xpass - always xfail ERROR test_example.py::test_error - assert 0 FAILED test_example.py::test_fail - assert 0 == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === @@ -380,7 +486,7 @@ More than one character can be used, so for example to only see failed and skipp $ pytest -rfs =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 6 items @@ -415,7 +521,7 @@ captured output: $ pytest -rpP =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 6 items @@ -478,7 +584,7 @@ integration servers, use this invocation: .. code-block:: bash - pytest --junitxml=path + pytest --junit-xml=path to create an XML file at ``path``. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst index a0c9968428c..b6466c491b4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/parametrize.rst @@ -56,7 +56,7 @@ them in turn: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 3 items @@ -167,7 +167,7 @@ Let's run this: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 3 items diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst index cae737e96ed..7d5bcd85a31 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/plugins.rst @@ -21,7 +21,7 @@ there is no need to activate it. Here is a little annotated list for some popular plugins: * :pypi:`pytest-django`: write tests - for :std:doc:`django <django:index>` apps, using pytest integration. + for `django <https://docs.djangoproject.com/>`_ apps, using pytest integration. * :pypi:`pytest-twisted`: write tests for `twisted <https://twistedmatrix.com/>`_ apps, starting a reactor and @@ -51,8 +51,8 @@ Here is a little annotated list for some popular plugins: * :pypi:`pytest-flakes`: check source code with pyflakes. -* :pypi:`oejskit`: - a plugin to run javascript unittests in live browsers. +* :pypi:`allure-pytest`: + report test results via `allure-framework <https://github.com/allure-framework/>`_. To see a complete list of all plugins with their latest testing status against different pytest and Python versions, please visit diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst index e2f59c77ae8..09a19766f99 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/skipping.rst @@ -47,8 +47,7 @@ which may be passed an optional ``reason``: .. code-block:: python @pytest.mark.skip(reason="no way of currently testing this") - def test_the_unknown(): - ... + def test_the_unknown(): ... Alternatively, it is also possible to skip imperatively during test execution or setup @@ -69,6 +68,7 @@ It is also possible to skip the whole module using .. code-block:: python import sys + import pytest if not sys.platform.startswith("win"): @@ -84,16 +84,15 @@ It is also possible to skip the whole module using If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on an interpreter earlier than Python3.6: +when run on an interpreter earlier than Python3.10: .. code-block:: python import sys - @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") - def test_function(): - ... + @pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") + def test_function(): ... If the condition evaluates to ``True`` during collection, the test function will be skipped, with the specified reason appearing in the summary when using ``-rs``. @@ -111,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module: @minversion - def test_function(): - ... + def test_function(): ... You can import the marker and reuse it in another test module: @@ -123,8 +121,7 @@ You can import the marker and reuse it in another test module: @minversion - def test_anotherfunction(): - ... + def test_anotherfunction(): ... For larger test suites it's usually a good idea to have one file where you define the markers which you then consistently apply @@ -231,8 +228,7 @@ expect a test to fail: .. code-block:: python @pytest.mark.xfail - def test_function(): - ... + def test_function(): ... This test will run but no traceback will be reported when it fails. Instead, terminal reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly @@ -274,8 +270,7 @@ that condition as the first parameter: .. code-block:: python @pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library") - def test_function(): - ... + def test_function(): ... Note that you have to pass a reason as well (see the parameter description at :ref:`pytest.mark.xfail ref`). @@ -288,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter: .. code-block:: python @pytest.mark.xfail(reason="known parser issue") - def test_function(): - ... + def test_function(): ... ``raises`` parameter @@ -301,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument. .. code-block:: python @pytest.mark.xfail(raises=RuntimeError) - def test_function(): - ... + def test_function(): ... Then the test will be reported as a regular failure if it fails with an exception not mentioned in ``raises``. @@ -316,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``: .. code-block:: python @pytest.mark.xfail(run=False) - def test_function(): - ... + def test_function(): ... This is specially useful for xfailing tests that are crashing the interpreter and should be investigated later. @@ -333,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True`` .. code-block:: python @pytest.mark.xfail(strict=True) - def test_function(): - ... + def test_function(): ... This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite. @@ -409,6 +400,7 @@ test instances when using parametrize: .. code-block:: python import sys + import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst index ebd74d42e90..3cc5152e992 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/tmp_path.rst @@ -8,9 +8,8 @@ How to use temporary directories and files in tests The ``tmp_path`` fixture ------------------------ -You can use the ``tmp_path`` fixture which will -provide a temporary directory unique to the test invocation, -created in the `base temporary directory`_. +You can use the ``tmp_path`` fixture which will provide a temporary directory +unique to each test function. ``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage: @@ -24,8 +23,8 @@ created in the `base temporary directory`_. d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" - p.write_text(CONTENT) - assert p.read_text() == CONTENT + p.write_text(CONTENT, encoding="utf-8") + assert p.read_text(encoding="utf-8") == CONTENT assert len(list(tmp_path.iterdir())) == 1 assert 0 @@ -36,7 +35,7 @@ Running this would result in a passed test except for the last $ pytest test_tmp_path.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -51,8 +50,8 @@ Running this would result in a passed test except for the last d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" - p.write_text(CONTENT) - assert p.read_text() == CONTENT + p.write_text(CONTENT, encoding="utf-8") + assert p.read_text(encoding="utf-8") == CONTENT assert len(list(tmp_path.iterdir())) == 1 > assert 0 E assert 0 @@ -62,6 +61,11 @@ Running this would result in a passed test except for the last FAILED test_tmp_path.py::test_create_file - assert 0 ============================ 1 failed in 0.12s ============================= +By default, ``pytest`` retains the temporary directory for the last 3 ``pytest`` +invocations. Concurrent invocations of the same test function are supported by +configuring the base temporary directory to be unique for each concurrent +run. See `temporary directory location and retention`_ for details. + .. _`tmp_path_factory example`: The ``tmp_path_factory`` fixture @@ -100,26 +104,45 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details. .. _tmpdir: The ``tmpdir`` and ``tmpdir_factory`` fixtures ---------------------------------------------------- +---------------------------------------------- The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path`` and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects -rather than standard :class:`pathlib.Path` objects. These days, prefer to -use ``tmp_path`` and ``tmp_path_factory``. +rather than standard :class:`pathlib.Path` objects. + +.. note:: + These days, it is preferred to use ``tmp_path`` and ``tmp_path_factory``. + + In order to help modernize old code bases, one can run pytest with the legacypath + plugin disabled: + + .. code-block:: bash + + pytest -p no:legacypath + + This will trigger errors on tests using the legacy paths. + It can also be permanently set as part of the :confval:`addopts` parameter in the + config file. See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>` API for details. -.. _`base temporary directory`: +.. _`temporary directory location and retention`: -The default base temporary directory ------------------------------------------------ +Temporary directory location and retention +------------------------------------------ Temporary directories are by default created as sub-directories of the system temporary directory. The base name will be ``pytest-NUM`` where -``NUM`` will be incremented with each test run. Moreover, entries older -than 3 temporary directories will be removed. +``NUM`` will be incremented with each test run. +By default, entries older than 3 temporary directories will be removed. +This behavior can be configured with :confval:`tmp_path_retention_count` and +:confval:`tmp_path_retention_policy`. + +Using the ``--basetemp`` +option will remove the directory before every run, effectively meaning the temporary directories +of only the most recent run will be kept. You can override the default temporary directory setting like this: @@ -133,7 +156,7 @@ You can override the default temporary directory setting like this: for that purpose only. When distributing tests on the local machine using ``pytest-xdist``, care is taken to -automatically configure a basetemp directory for the sub processes such that all temporary -data lands below a single per-test run basetemp directory. +automatically configure a `basetemp` directory for the sub processes such that all temporary +data lands below a single per-test run temporary directory. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst index bff75110778..508aebde016 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/unittest.rst @@ -27,12 +27,15 @@ Almost all ``unittest`` features are supported: * ``setUpClass/tearDownClass``; * ``setUpModule/tearDownModule``; +.. _`pytest-subtests`: https://github.com/pytest-dev/pytest-subtests .. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol +Additionally, :ref:`subtests <python:subtests>` are supported by the +`pytest-subtests`_ plugin. + Up to this point pytest does not have support for the following features: * `load_tests protocol`_; -* :ref:`subtests <python:subtests>`; Benefits out of the box ----------------------- @@ -115,6 +118,7 @@ fixture definition: # content of test_unittest_db.py import unittest + import pytest @@ -136,7 +140,7 @@ the ``self.db`` values in the traceback: $ pytest test_unittest_db.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items @@ -153,7 +157,7 @@ the ``self.db`` values in the traceback: E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001> E assert 0 - test_unittest_db.py:10: AssertionError + test_unittest_db.py:11: AssertionError ___________________________ MyTest.test_method2 ____________________________ self = <test_unittest_db.MyTest testMethod=test_method2> @@ -163,7 +167,7 @@ the ``self.db`` values in the traceback: E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001> E assert 0 - test_unittest_db.py:13: AssertionError + test_unittest_db.py:14: AssertionError ========================= short test summary info ========================== FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft... FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft... @@ -194,19 +198,19 @@ creation of a per-test temporary directory: .. code-block:: python # content of test_unittest_cleandir.py - import os - import pytest import unittest + import pytest + class MyTest(unittest.TestCase): @pytest.fixture(autouse=True) def initdir(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory - tmp_path.joinpath("samplefile.ini").write_text("# testdata") + tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8") def test_method(self): - with open("samplefile.ini") as f: + with open("samplefile.ini", encoding="utf-8") as f: s = f.read() assert "testdata" in s diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/usage.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/usage.rst index 3522b258dce..fe46fad2db5 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/usage.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/usage.rst @@ -17,7 +17,8 @@ in the current directory and its subdirectories. More generally, pytest follows Specifying which tests to run ------------------------------ -Pytest supports several ways to run and select tests from the command-line. +Pytest supports several ways to run and select tests from the command-line or from a file +(see below for :ref:`reading arguments from file <args-from-file>`). **Run tests in a module** @@ -35,31 +36,43 @@ Pytest supports several ways to run and select tests from the command-line. .. code-block:: bash - pytest -k "MyClass and not method" + pytest -k 'MyClass and not method' This will run tests which contain names that match the given *string expression* (case-insensitive), which can include Python operators that use filenames, class names and function names as variables. The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``. +Use ``""`` instead of ``''`` in expression when running this on Windows .. _nodeids: -**Run tests by node ids** +**Run tests by collection arguments** -Each collected test is assigned a unique ``nodeid`` which consist of the module filename followed -by specifiers like class names, function names and parameters from parametrization, separated by ``::`` characters. +Pass the module filename relative to the working directory, followed by specifiers like the class name and function name +separated by ``::`` characters, and parameters from parameterization enclosed in ``[]``. To run a specific test within a module: .. code-block:: bash - pytest test_mod.py::test_func + pytest tests/test_mod.py::test_func +To run all tests in a class: -Another example specifying a test method in the command line: +.. code-block:: bash + + pytest tests/test_mod.py::TestClass + +Specifying a specific test method: .. code-block:: bash - pytest test_mod.py::TestClass::test_method + pytest tests/test_mod.py::TestClass::test_method + +Specifying a specific parametrization of a test: + +.. code-block:: bash + + pytest tests/test_mod.py::test_func[x1,y2] **Run tests by marker expressions** @@ -79,6 +92,28 @@ For more information see :ref:`marks <mark>`. This will import ``pkg.testing`` and use its filesystem location to find and run tests from. +.. _args-from-file: + +**Read arguments from file** + +.. versionadded:: 8.2 + +All of the above can be read from a file using the ``@`` prefix: + +.. code-block:: bash + + pytest @tests_to_run.txt + +where ``tests_to_run.txt`` contains an entry per line, e.g.: + +.. code-block:: text + + tests/test_file.py + tests/test_mod.py::test_func[x1,y2] + tests/test_mod.py::TestClass + -m slow + +This file can also be generated using ``pytest --collect-only -q`` and modified as needed. Getting help on version, option names, environment variables -------------------------------------------------------------- @@ -172,7 +207,8 @@ You can invoke ``pytest`` from Python code directly: this acts as if you would call "pytest" from the command line. It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead. -You can pass in options and arguments: +If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable. +You can pass in options and arguments explicitly: .. code-block:: python @@ -183,9 +219,10 @@ You can specify additional plugins to ``pytest.main``: .. code-block:: python # content of myinvoke.py - import pytest import sys + import pytest + class MyPlugin: def pytest_sessionfinish(self): diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst index f615fced861..f4c00d04fda 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_hook_functions.rst @@ -56,23 +56,17 @@ The remaining hook functions will not be called in this case. .. _`hookwrapper`: -hookwrapper: executing around other hooks +hook wrappers: executing around other hooks ------------------------------------------------- -.. currentmodule:: _pytest.core - - - pytest plugins can implement hook wrappers which wrap the execution of other hook implementations. A hook wrapper is a generator function which yields exactly once. When pytest invokes hooks it first executes hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook -implementations and return their result to the yield point in the form of -a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or -exception info. The yield point itself will thus typically not raise -exceptions (unless there are bugs). +implementations and return their result to the yield point, or will +propagate an exception if they raised. Here is an example definition of a hook wrapper: @@ -81,26 +75,35 @@ Here is an example definition of a hook wrapper: import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() - outcome = yield - # outcome.excinfo may be None or a (cls, val, tb) tuple + # If the outcome is an exception, will raise the exception. + res = yield + + new_res = post_process_result(res) - res = outcome.get_result() # will raise if outcome was exception + # Override the return value to the plugin system. + return new_res - post_process_result(res) +The hook wrapper needs to return a result for the hook, or raise an exception. - outcome.force_result(new_res) # to override the return value to the plugin system +In many cases, the wrapper only needs to perform tracing or other side effects +around the actual hook implementations, in which case it can return the result +value of the ``yield``. The simplest (though useless) hook wrapper is +``return (yield)``. -Note that hook wrappers don't return results themselves, they merely -perform tracing or other side effects around the actual hook implementations. -If the result of the underlying hook is a mutable object, they may modify -that result but it's probably better to avoid it. +In other cases, the wrapper wants the adjust or adapt the result, in which case +it can return a new value. If the result of the underlying hook is a mutable +object, the wrapper may modify that result, but it's probably better to avoid it. + +If the hook implementation failed with an exception, the wrapper can handle that +exception using a ``try-catch-finally`` around the ``yield``, by propagating it, +suppressing it, or raising a different exception entirely. For more information, consult the -:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`. +:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`. .. _plugin-hookorder: @@ -130,11 +133,14 @@ after others, i.e. the position in the ``N``-sized list of functions: # Plugin 3 - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(items): # will execute even before the tryfirst one above! - outcome = yield - # will execute after all non-hookwrappers executed + try: + return (yield) + finally: + # will execute after all non-wrappers executed + ... Here is the order of execution: @@ -149,13 +155,13 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates - the result from calling the non-wrappers. Wrappers shall not modify the result. + point. The yield receives the result from calling the non-wrappers, or raises + an exception if the non-wrappers raised. -It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with -``hookwrapper=True`` in which case it will influence the ordering of hookwrappers -among each other. +It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers +in which case it will influence the ordering of hook wrappers among each other. +.. _`declaringhooks`: Declaring new hooks ------------------------ @@ -165,13 +171,11 @@ Declaring new hooks This is a quick overview on how to add new hooks and how they work in general, but a more complete overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__. -.. currentmodule:: _pytest.hookspec - Plugins and ``conftest.py`` files may declare new hooks that can then be implemented by other plugins in order to alter behaviour or interact with the new plugin: -.. autofunction:: pytest_addhooks +.. autofunction:: _pytest.hookspec.pytest_addhooks :noindex: Hooks are usually declared as do-nothing functions that contain only @@ -194,7 +198,7 @@ class or module can then be passed to the ``pluginmanager`` using the ``pytest_a .. code-block:: python def pytest_addhooks(pluginmanager): - """ This example assumes the hooks are grouped in the 'sample_hook' module. """ + """This example assumes the hooks are grouped in the 'sample_hook' module.""" from my_app.tests import sample_hook pluginmanager.add_hookspecs(sample_hook) @@ -249,18 +253,19 @@ and use pytest_addoption as follows: # contents of hooks.py + # Use firstresult=True because we only want one plugin to define this # default value @hookspec(firstresult=True) def pytest_config_file_default_value(): - """ Return the default value for the config file command line option. """ + """Return the default value for the config file command line option.""" # contents of myplugin.py def pytest_addhooks(pluginmanager): - """ This example assumes the hooks are grouped in the 'hooks' module. """ + """This example assumes the hooks are grouped in the 'hooks' module.""" from . import hooks pluginmanager.add_hookspecs(hooks) diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst index b2d2b6563d6..4bb6d183333 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/writing_plugins.rst @@ -46,24 +46,18 @@ Plugin discovery order at tool startup 5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable. -6. by loading all :file:`conftest.py` files as inferred by the command line - invocation: +6. by loading all "initial ":file:`conftest.py` files: - - if no test paths are specified, use the current dir as a test path - - if exists, load ``conftest.py`` and ``test*/conftest.py`` relative - to the directory part of the first test path. After the ``conftest.py`` - file is loaded, load all plugins specified in its - :globalvar:`pytest_plugins` variable if present. + - determine the test paths: specified on the command line, otherwise in + :confval:`testpaths` if defined and running from the rootdir, otherwise the + current dir + - for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative + to the directory part of the test path, if exist. Before a ``conftest.py`` + file is loaded, load ``conftest.py`` files in all of its parent directories. + After a ``conftest.py`` file is loaded, recursively load all plugins specified + in its :globalvar:`pytest_plugins` variable if present. - Note that pytest does not find ``conftest.py`` files in deeper nested - sub directories at tool startup. It is usually a good idea to keep - your ``conftest.py`` file in the top level test or project root directory. -7. by recursively loading all plugins specified by the - :globalvar:`pytest_plugins` variable in ``conftest.py`` files. - - -.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ .. _`conftest.py plugins`: .. _`localplugin`: .. _`local conftest plugins`: @@ -108,9 +102,9 @@ Here is how you might run it:: See also: :ref:`pythonpath`. .. note:: - Some hooks should be implemented only in plugins or conftest.py files situated at the - tests root directory due to how pytest discovers plugins during startup, - see the documentation of each hook for details. + Some hooks cannot be implemented in conftest.py files which are not + :ref:`initial <pluginorder>` due to how pytest discovers plugins during + startup. See the documentation of each hook for details. Writing your own plugin ----------------------- @@ -147,29 +141,32 @@ Making your plugin installable by others If you want to make your plugin externally available, you may define a so-called entry point for your distribution so -that ``pytest`` finds your plugin module. Entry points are -a feature that is provided by :std:doc:`setuptools:index`. pytest looks up -the ``pytest11`` entrypoint to discover its -plugins and you can thus make your plugin available by defining -it in your setuptools-invocation: +that ``pytest`` finds your plugin module. Entry points are +a feature that is provided by :std:doc:`setuptools <setuptools:index>`. -.. sourcecode:: python +pytest looks up the ``pytest11`` entrypoint to discover its +plugins, thus you can make your plugin available by defining +it in your ``pyproject.toml`` file. + +.. sourcecode:: toml + + # sample ./pyproject.toml file + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" - # sample ./setup.py file - from setuptools import setup + [project] + name = "myproject" + classifiers = [ + "Framework :: Pytest", + ] - setup( - name="myproject", - packages=["myproject"], - # the following makes a plugin available to pytest - entry_points={"pytest11": ["name_of_plugin = myproject.pluginmodule"]}, - # custom PyPI classifier for pytest plugins - classifiers=["Framework :: Pytest"], - ) + [project.entry-points.pytest11] + myproject = "myproject.pluginmodule" If a package is installed this way, ``pytest`` will load ``myproject.pluginmodule`` as a plugin which can define -:ref:`hooks <hook-reference>`. +:ref:`hooks <hook-reference>`. Confirm registration with ``pytest --trace-config`` .. note:: @@ -367,7 +364,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello def _hello(name=None): if not name: name = request.config.getoption("name") - return "Hello {name}!".format(name=name) + return f"Hello {name}!" return _hello @@ -445,8 +442,9 @@ in our ``pytest.ini`` to tell pytest where to look for example files. $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y - rootdir: /home/sweet/project, configfile: pytest.ini + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + configfile: pytest.ini collected 2 items test_example.py .. [100%] diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst index 5a97b2c85f1..3de6681ff8f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/how-to/xunit_setup.rst @@ -32,7 +32,7 @@ which will usually be called once for all the functions: .. code-block:: python def setup_module(module): - """ setup any state specific to the execution of the given module.""" + """setup any state specific to the execution of the given module.""" def teardown_module(module): @@ -63,6 +63,8 @@ and after all test methods of the class are called: setup_class. """ +.. _xunit-method-setup: + Method and function level setup/teardown ----------------------------------------------- diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/index.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/index.rst index d1b3d2e8a08..83eb27b0a53 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/index.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/index.rst @@ -1,11 +1,14 @@ :orphan: -.. - .. sidebar:: Next Open Trainings +.. sidebar:: Next Open Trainings and Events - - `Professional Testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, February 1st to 3rd, 2022, Leipzig (Germany) and remote. + - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training): + * **June 11th to 13th 2024**, Remote + * **March 4th to 6th 2025**, Leipzig, Germany / Remote + - `pytest development sprint <https://github.com/pytest-dev/sprint>`_, **June 17th -- 22nd 2024** + - pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague - Also see `previous talks and blogposts <talks.html>`_. + Also see :doc:`previous talks and blogposts <talks>`. .. _features: @@ -18,12 +21,10 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. -**Pythons**: ``pytest`` requires: Python 3.6, 3.7, 3.8, 3.9, or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. **PyPI package name**: :pypi:`pytest` -**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_ - A quick example --------------- @@ -45,7 +46,7 @@ To execute it: $ pytest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 1 item @@ -77,17 +78,17 @@ Features - :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources -- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box +- Can run :ref:`unittest <unittest>` (including trial) test suites out of the box -- Python 3.6+ and PyPy 3 +- Python 3.8+ or PyPy 3 -- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community +- Rich plugin architecture, with over 1300+ :ref:`external plugins <plugin-list>` and thriving community Documentation ------------- -* :ref:`Get started <get-started>` - install pytest and grasp its basics just twenty minutes +* :ref:`Get started <get-started>` - install pytest and grasp its basics in just twenty minutes * :ref:`How-to guides <how-to>` - step-by-step guides, covering a vast range of use-cases and needs * :ref:`Reference guides <reference>` - includes the complete pytest API reference, lists of plugins and more * :ref:`Explanation <explanation>` - background, discussion of key topics, answers to higher-level questions @@ -99,11 +100,6 @@ Bugs/Requests Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features. -Changelog ---------- - -Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each version. - Support pytest -------------- @@ -136,13 +132,3 @@ Security pytest has never been associated with a security vulnerability, but in any case, to report a security vulnerability please use the `Tidelift security contact <https://tidelift.com/security>`_. Tidelift will coordinate the fix and disclosure. - - -License -------- - -Copyright Holger Krekel and others, 2004. - -Distributed under the terms of the `MIT`_ license, pytest is free and open source software. - -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/naming20.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/naming20.rst index 5a81df2698d..11213066384 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/naming20.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/naming20.rst @@ -8,7 +8,7 @@ If you used older version of the ``py`` distribution (which included the py.test command line tool and Python name space) you accessed helpers and possibly collection classes through the ``py.test`` Python namespaces. The new ``pytest`` -Python module flaty provides the same objects, following +Python module flatly provides the same objects, following these renaming rules:: py.test.XYZ -> pytest.XYZ diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst deleted file mode 100644 index 660b078e30e..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/py27-py34-deprecation.rst +++ /dev/null @@ -1,99 +0,0 @@ -Python 2.7 and 3.4 support -========================== - -It is demanding on the maintainers of an open source project to support many Python versions, as -there's extra cost of keeping code compatible between all versions, while holding back on -features only made possible on newer Python versions. - -In case of Python 2 and 3, the difference between the languages makes it even more prominent, -because many new Python 3 features cannot be used in a Python 2/3 compatible code base. - -Python 2.7 EOL has been reached :pep:`in 2020 <0373#maintenance-releases>`, with -the last release made in April, 2020. - -Python 3.4 EOL has been reached :pep:`in 2019 <0429#release-schedule>`, with the last release made in March, 2019. - -For those reasons, in Jun 2019 it was decided that **pytest 4.6** series will be the last to support Python 2.7 and 3.4. - -What this means for general users ---------------------------------- - -Thanks to the `python_requires`_ setuptools option, -Python 2.7 and Python 3.4 users using a modern pip version -will install the last pytest 4.6.X version automatically even if 5.0 or later versions -are available on PyPI. - -Users should ensure they are using the latest pip and setuptools versions for this to work. - -Maintenance of 4.6.X versions ------------------------------ - -Until January 2020, the pytest core team ported many bug-fixes from the main release into the -``4.6.x`` branch, with several 4.6.X releases being made along the year. - -From now on, the core team will **no longer actively backport patches**, but the ``4.6.x`` -branch will continue to exist so the community itself can contribute patches. - -The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020** -(but consider that date as a ballpark, after that date the team might still decide to make new releases -for critical bugs). - -.. _`python_requires`: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires - -Technical aspects -~~~~~~~~~~~~~~~~~ - -(This section is a transcript from :issue:`5275`). - -In this section we describe the technical aspects of the Python 2.7 and 3.4 support plan. - -.. _what goes into 4.6.x releases: - -What goes into 4.6.X releases -+++++++++++++++++++++++++++++ - -New 4.6.X releases will contain bug fixes only. - -When will 4.6.X releases happen -+++++++++++++++++++++++++++++++ - -New 4.6.X releases will happen after we have a few bugs in place to release, or if a few weeks have -passed (say a single bug has been fixed a month after the latest 4.6.X release). - -No hard rules here, just ballpark. - -Who will handle applying bug fixes -++++++++++++++++++++++++++++++++++ - -We core maintainers expect that people still using Python 2.7/3.4 and being affected by -bugs to step up and provide patches and/or port bug fixes from the active branches. - -We will be happy to guide users interested in doing so, so please don't hesitate to ask. - -**Backporting changes into 4.6** - -Please follow these instructions: - -#. ``git fetch --all --prune`` - -#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here - -#. Locate the merge commit on the PR, in the *merged* message, for example: - - nicoddemus merged commit 0f8b462 into pytest-dev:features - -#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``). - -#. Open a PR targeting ``4.6.x``: - - * Prefix the message with ``[4.6]`` so it is an obvious backport - * Delete the PR body, it usually contains a duplicate commit message. - -**Providing new PRs to 4.6** - -Fresh pull requests to ``4.6.x`` will be accepted provided that -the equivalent code in the active branches does not contain that bug (for example, a bug is specific -to Python 2 only). - -Bug fixes that also happen in the mainstream version should be first fixed -there, and then backported as per instructions above. diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/customize.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/customize.rst index fe10ca066b2..cab1117266f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/customize.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/customize.rst @@ -29,9 +29,11 @@ pytest.ini ``pytest.ini`` files take precedence over other files, even when empty. +Alternatively, the hidden version ``.pytest.ini`` can be used. + .. code-block:: ini - # pytest.ini + # pytest.ini or .pytest.ini [pytest] minversion = 6.0 addopts = -ra -q @@ -88,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se setup.cfg ~~~~~~~~~ -``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <distutils/configfile>`, and can also be used to hold pytest configuration +``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration if they have a ``[tool:pytest]`` section. .. code-block:: ini @@ -175,13 +177,20 @@ Files will only be matched for configuration if: * ``tox.ini``: contains a ``[pytest]`` section. * ``setup.cfg``: contains a ``[tool:pytest]`` section. +Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case +even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``). + The files are considered in the order above. Options from multiple ``configfiles`` candidates are never merged - the first match wins. +The configuration file also determines the value of the ``rootpath``. + The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) will subsequently carry these attributes: -- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. +- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as + a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing + per-testrun information. - :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None`` (it is named ``inipath`` for historical reasons). @@ -191,9 +200,7 @@ will subsequently carry these attributes: versions of the older ``config.rootdir`` and ``config.inifile``, which have type ``py.path.local``, and still exist for backward compatibility. -The ``rootdir`` is used as a reference directory for constructing test -addresses ("nodeids") and can be used also by plugins for storing -per-testrun information. + Example: diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst index d25979ab95d..dff93a035ef 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/fixtures.rst @@ -11,9 +11,6 @@ Fixtures reference .. seealso:: :ref:`about-fixtures` .. seealso:: :ref:`how-to-fixtures` - -.. currentmodule:: _pytest.python - .. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection @@ -42,7 +39,7 @@ Built-in fixtures Store and retrieve values across pytest runs. :fixture:`doctest_namespace` - Provide a dict injected into the docstests namespace. + Provide a dict injected into the doctests namespace. :fixture:`monkeypatch` Temporarily modify classes, functions, dictionaries, @@ -76,15 +73,13 @@ Built-in fixtures :class:`pathlib.Path` objects. :fixture:`tmpdir` - Provide a :class:`py.path.local` object to a temporary + Provide a `py.path.local <https://py.readthedocs.io/en/latest/path.html>`_ object to a temporary directory which is unique to each test function; replaced by :fixture:`tmp_path`. - .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html - :fixture:`tmpdir_factory` Make session-scoped temporary directories and return - :class:`py.path.local` objects; + ``py.path.local`` objects; replaced by :fixture:`tmp_path_factory`. @@ -98,7 +93,7 @@ Fixture availability is determined from the perspective of the test. A fixture is only available for tests to request if they are in the scope that fixture is defined in. If a fixture is defined inside a class, it can only be requested by tests inside that class. But if a fixture is defined inside the global scope of -the module, than every test in that module, even if it's defined inside a class, +the module, then every test in that module, even if it's defined inside a class, can request it. Similarly, a test can also only be affected by an autouse fixture if that test @@ -335,7 +330,7 @@ For example: .. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py -If we map out what depends on what, we get something that look like this: +If we map out what depends on what, we get something that looks like this: .. image:: /example/fixtures/test_fixtures_order_dependencies.* :align: center diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/index.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/index.rst index d9648400317..ee1b2e6214d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/index.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/index.rst @@ -8,8 +8,8 @@ Reference guides .. toctree:: :maxdepth: 1 + reference fixtures - plugin_list customize - reference exit-codes + plugin_list diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst index ebf40091369..e1d1e3ec24a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/plugin_list.rst @@ -1,1005 +1,1548 @@ +.. Note this file is autogenerated by scripts/update-plugin-list.py - usually weekly via github action + .. _plugin-list: -Plugin List -=========== +Pytest Plugin List +================== + +Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_. +It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects. +Packages classified as inactive are excluded. + +For detailed insights into how this list is generated, +please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_. + +.. warning:: + + Please be aware that this list is not a curated collection of projects + and does not undergo a systematic review process. + It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins. + + Do not presume any endorsement from the ``pytest`` project or its developers, + and always conduct your own quality assessment before incorporating any of these plugins into your own projects. -PyPI projects that match "pytest-\*" are considered plugins and are listed -automatically. Packages classified as inactive are excluded. .. The following conditional uses a different format for this list when creating a PDF, because otherwise the table gets far too wide for the page. -This list contains 963 plugins. +This list contains 1448 plugins. .. only:: not latex - =============================================== ======================================================================================================================================================================== ============== ===================== ================================================ - name summary last release status requires - =============================================== ======================================================================================================================================================================== ============== ===================== ================================================ - :pypi:`pytest-accept` A pytest-plugin for updating doctest outputs Nov 22, 2021 N/A pytest (>=6,<7) - :pypi:`pytest-adaptavist` pytest plugin for generating test execution results within Jira Test Management (tm4j) Nov 30, 2021 N/A pytest (>=5.4.0) - :pypi:`pytest-addons-test` 用于测试pytest的插件 Aug 02, 2021 N/A pytest (>=6.2.4,<7.0.0) - :pypi:`pytest-adf` Pytest plugin for writing Azure Data Factory integration tests May 10, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-adf-azure-identity` Pytest plugin for writing Azure Data Factory integration tests Mar 06, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-agent` Service that exposes a REST API that can be used to interract remotely with Pytest. It is shipped with a dashboard that enables running tests in a more convenient way. Nov 25, 2021 N/A N/A - :pypi:`pytest-aggreport` pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. Mar 07, 2021 4 - Beta pytest (>=6.2.2) - :pypi:`pytest-aio` Pytest plugin for testing async python code Oct 20, 2021 4 - Beta pytest - :pypi:`pytest-aiofiles` pytest fixtures for writing aiofiles tests with pyfakefs May 14, 2017 5 - Production/Stable N/A - :pypi:`pytest-aiohttp` pytest plugin for aiohttp support Dec 05, 2017 N/A pytest - :pypi:`pytest-aiohttp-client` Pytest \`client\` fixture for the Aiohttp Nov 01, 2020 N/A pytest (>=6) - :pypi:`pytest-aioresponses` py.test integration for aioresponses Jul 29, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest Dec 04, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) - :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Dec 02, 2021 N/A pytest (>=1.0) - :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest - :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) - :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) - :pypi:`pytest-allure-dsl` pytest plugin to test case doc string dls instructions Oct 25, 2020 4 - Beta pytest - :pypi:`pytest-allure-spec-coverage` The pytest plugin aimed to display test coverage of the specs(requirements) in Allure Oct 26, 2021 N/A pytest - :pypi:`pytest-alphamoon` Static code checks used at Alphamoon Oct 21, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-android` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Feb 21, 2019 3 - Alpha pytest - :pypi:`pytest-anki` A pytest plugin for testing Anki add-ons Oct 14, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-annotate` pytest-annotate: Generate PyAnnotate annotations from your pytest tests. Nov 29, 2021 3 - Alpha pytest (<7.0.0,>=3.2.0) - :pypi:`pytest-ansible` Plugin for py.test to simplify calling ansible modules from tests or fixtures May 25, 2021 5 - Production/Stable N/A - :pypi:`pytest-ansible-playbook` Pytest fixture which runs given ansible playbook file. Mar 08, 2019 4 - Beta N/A - :pypi:`pytest-ansible-playbook-runner` Pytest fixture which runs given ansible playbook file. Dec 02, 2020 4 - Beta pytest (>=3.1.0) - :pypi:`pytest-antilru` Bust functools.lru_cache when running pytest to avoid test pollution Apr 11, 2019 5 - Production/Stable pytest - :pypi:`pytest-anyio` The pytest anyio plugin is built into anyio. You don't need this package. Jun 29, 2021 N/A pytest - :pypi:`pytest-anything` Pytest fixtures to assert anything and something Feb 18, 2021 N/A N/A - :pypi:`pytest-aoc` Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures Nov 23, 2021 N/A pytest ; extra == 'test' - :pypi:`pytest-api` PyTest-API Python Web Framework built for testing purposes. May 04, 2021 N/A N/A - :pypi:`pytest-apistellar` apistellar plugin for pytest. Jun 18, 2019 N/A N/A - :pypi:`pytest-appengine` AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A - :pypi:`pytest-appium` Pytest plugin for appium Dec 05, 2019 N/A N/A - :pypi:`pytest-approvaltests` A plugin to use approvaltests with pytest Feb 07, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-argus` pyest results colection plugin Jun 24, 2021 5 - Production/Stable pytest (>=6.2.4) - :pypi:`pytest-arraydiff` pytest plugin to help with comparing array output from tests Dec 06, 2018 4 - Beta pytest - :pypi:`pytest-asgi-server` Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) - :pypi:`pytest-asptest` test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A - :pypi:`pytest-assertutil` pytest-assertutil May 10, 2019 N/A N/A - :pypi:`pytest-assert-utils` Useful assertion utilities for use with pytest Sep 21, 2021 3 - Alpha N/A - :pypi:`pytest-assume` A pytest plugin that allows multiple failures per test Jun 24, 2021 N/A pytest (>=2.7) - :pypi:`pytest-ast-back-to-python` A plugin for pytest devs to view how assertion rewriting recodes the AST Sep 29, 2019 4 - Beta N/A - :pypi:`pytest-astropy` Meta-package containing dependencies for testing Sep 21, 2021 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-astropy-header` pytest plugin to add diagnostic information to the header of the test output Dec 18, 2019 3 - Alpha pytest (>=2.8) - :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest - :pypi:`pytest-asyncio` Pytest support for asyncio. Oct 15, 2021 4 - Beta pytest (>=5.4.0) - :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Oct 12, 2021 4 - Beta N/A - :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) - :pypi:`pytest-async-mongodb` pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) - :pypi:`pytest-async-sqlalchemy` Database testing fixtures using the SQLAlchemy asyncio API Oct 07, 2021 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-atomic` Skip rest of tests if previous test failed. Nov 24, 2018 4 - Beta N/A - :pypi:`pytest-attrib` pytest plugin to select tests based on attributes similar to the nose-attrib plugin May 24, 2016 4 - Beta N/A - :pypi:`pytest-austin` Austin plugin for pytest Oct 11, 2020 4 - Beta N/A - :pypi:`pytest-autochecklog` automatically check condition and log all the checks Apr 25, 2015 4 - Beta N/A - :pypi:`pytest-automation` pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. Oct 01, 2021 N/A pytest - :pypi:`pytest-automock` Pytest plugin for automatical mocks creation Apr 22, 2020 N/A pytest ; extra == 'dev' - :pypi:`pytest-auto-parametrize` pytest plugin: avoid repeating arguments in parametrize Oct 02, 2016 3 - Alpha N/A - :pypi:`pytest-autotest` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Aug 25, 2021 N/A pytest - :pypi:`pytest-avoidance` Makes pytest skip tests that don not need rerunning May 23, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-aws` pytest plugin for testing AWS resource configurations Oct 04, 2017 4 - Beta N/A - :pypi:`pytest-aws-config` Protect your AWS credentials in unit tests May 28, 2021 N/A N/A - :pypi:`pytest-axe` pytest plugin for axe-selenium-python Nov 12, 2018 N/A pytest (>=3.0.0) - :pypi:`pytest-azurepipelines` Formatting PyTest output for Azure Pipelines UI Jul 23, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-bandit` A bandit plugin for pytest Feb 23, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-base-url` pytest plugin for URL based testing Jun 19, 2020 5 - Production/Stable pytest (>=2.7.3) - :pypi:`pytest-bdd` BDD for pytest Oct 25, 2021 6 - Mature pytest (>=4.3) - :pypi:`pytest-bdd-splinter` Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) - :pypi:`pytest-bdd-web` A simple plugin to use with pytest Jan 02, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-bdd-wrappers` Feb 11, 2020 2 - Pre-Alpha N/A - :pypi:`pytest-beakerlib` A pytest plugin that reports test results to the BeakerLib framework Mar 17, 2017 5 - Production/Stable pytest - :pypi:`pytest-beds` Fixtures for testing Google Appengine (GAE) apps Jun 07, 2016 4 - Beta N/A - :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A - :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Apr 17, 2021 5 - Production/Stable pytest (>=3.8) - :pypi:`pytest-bg-process` Pytest plugin to initialize background process Aug 17, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-bigchaindb` A BigchainDB plugin for pytest. Aug 17, 2021 4 - Beta N/A - :pypi:`pytest-bigquery-mock` Provides a mock fixture for python bigquery client Aug 05, 2021 N/A pytest (>=5.0) - :pypi:`pytest-black` A pytest plugin to enable format checking with black Oct 05, 2020 4 - Beta N/A - :pypi:`pytest-black-multipy` Allow '--black' on older Pythons Jan 14, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' - :pypi:`pytest-blame` A pytest plugin helps developers to debug by providing useful commits history. May 04, 2019 N/A pytest (>=4.4.0) - :pypi:`pytest-blender` Blender Pytest plugin. Oct 29, 2021 N/A pytest (==6.2.5) ; extra == 'dev' - :pypi:`pytest-blink1` Pytest plugin to emit notifications via the Blink(1) RGB LED Jan 07, 2018 4 - Beta N/A - :pypi:`pytest-blockage` Disable network requests during a test run. Feb 13, 2019 N/A pytest - :pypi:`pytest-blocker` pytest plugin to mark a test as blocker and skip all other tests Sep 07, 2015 4 - Beta N/A - :pypi:`pytest-board` Local continuous test runner with pytest and watchdog. Jan 20, 2019 N/A N/A - :pypi:`pytest-bpdb` A py.test plug-in to enable drop to bpdb debugger on test failure. Jan 19, 2015 2 - Pre-Alpha N/A - :pypi:`pytest-bravado` Pytest-bravado automatically generates from OpenAPI specification client fixtures. Jul 19, 2021 N/A N/A - :pypi:`pytest-breakword` Use breakword with pytest Aug 04, 2021 N/A pytest (>=6.2.4,<7.0.0) - :pypi:`pytest-breed-adapter` A simple plugin to connect with breed-server Nov 07, 2018 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-briefcase` A pytest plugin for running tests on a Briefcase project. Jun 14, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-browser` A pytest plugin for console based browser test selection just after the collection phase Dec 10, 2016 3 - Alpha N/A - :pypi:`pytest-browsermob-proxy` BrowserMob proxy plugin for py.test. Jun 11, 2013 4 - Beta N/A - :pypi:`pytest-browserstack-local` \`\`py.test\`\` plugin to run \`\`BrowserStackLocal\`\` in background. Feb 09, 2018 N/A N/A - :pypi:`pytest-bug` Pytest plugin for marking tests as a bug Jun 02, 2020 5 - Production/Stable pytest (>=3.6.0) - :pypi:`pytest-bugtong-tag` pytest-bugtong-tag is a plugin for pytest Apr 23, 2021 N/A N/A - :pypi:`pytest-bugzilla` py.test bugzilla integration plugin May 05, 2010 4 - Beta N/A - :pypi:`pytest-bugzilla-notifier` A plugin that allows you to execute create, update, and read information from BugZilla bugs Jun 15, 2018 4 - Beta pytest (>=2.9.2) - :pypi:`pytest-buildkite` Plugin for pytest that automatically publishes coverage and pytest report annotations to Buildkite. Jul 13, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-builtin-types` Nov 17, 2021 N/A pytest - :pypi:`pytest-bwrap` Run your tests in Bubblewrap sandboxes Oct 26, 2018 3 - Alpha N/A - :pypi:`pytest-cache` pytest plugin with mechanisms for caching across test runs Jun 04, 2013 3 - Alpha N/A - :pypi:`pytest-cache-assert` Cache assertion data to simplify regression testing of complex serializable data Nov 03, 2021 4 - Beta pytest (>=5) - :pypi:`pytest-cagoule` Pytest plugin to only run tests affected by changes Jan 01, 2020 3 - Alpha N/A - :pypi:`pytest-camel-collect` Enable CamelCase-aware pytest class collection Aug 02, 2020 N/A pytest (>=2.9) - :pypi:`pytest-canonical-data` A plugin which allows to compare results with canonical results, based on previous runs May 08, 2020 2 - Pre-Alpha pytest (>=3.5.0) - :pypi:`pytest-caprng` A plugin that replays pRNG state on failure. May 02, 2018 4 - Beta N/A - :pypi:`pytest-capture-deprecatedwarnings` pytest plugin to capture all deprecatedwarnings and put them in one file Apr 30, 2019 N/A N/A - :pypi:`pytest-capturelogs` A sample Python project Sep 11, 2021 3 - Alpha N/A - :pypi:`pytest-cases` Separate test code from test cases in pytest. Nov 08, 2021 5 - Production/Stable N/A - :pypi:`pytest-cassandra` Cassandra CCM Test Fixtures for pytest Nov 04, 2017 1 - Planning N/A - :pypi:`pytest-catchlog` py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) - :pypi:`pytest-catch-server` Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A - :pypi:`pytest-celery` pytest-celery a shim pytest plugin to enable celery.contrib.pytest May 06, 2021 N/A N/A - :pypi:`pytest-chainmaker` pytest plugin for chainmaker Oct 15, 2021 N/A N/A - :pypi:`pytest-chalice` A set of py.test fixtures for AWS Chalice Jul 01, 2020 4 - Beta N/A - :pypi:`pytest-change-report` turn . into √,turn F into x Sep 14, 2020 N/A pytest - :pypi:`pytest-chdir` A pytest fixture for changing current working directory Jan 28, 2020 N/A pytest (>=5.0.0,<6.0.0) - :pypi:`pytest-checkdocs` check the README when running tests Jul 31, 2021 5 - Production/Stable pytest (>=4.6) ; extra == 'testing' - :pypi:`pytest-checkipdb` plugin to check if there are ipdb debugs left Jul 22, 2020 5 - Production/Stable pytest (>=2.9.2) - :pypi:`pytest-check-links` Check links in files Jul 29, 2020 N/A pytest (>=4.6) - :pypi:`pytest-check-mk` pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest - :pypi:`pytest-circleci` py.test plugin for CircleCI May 03, 2019 N/A N/A - :pypi:`pytest-circleci-parallelized` Parallelize pytest across CircleCI workers. Mar 26, 2019 N/A N/A - :pypi:`pytest-ckan` Backport of CKAN 2.9 pytest plugin and fixtures to CAKN 2.8 Apr 28, 2020 4 - Beta pytest - :pypi:`pytest-clarity` A plugin providing an alternative, colourful diff output for failing assertions. Jun 11, 2021 N/A N/A - :pypi:`pytest-cldf` Easy quality control for CLDF datasets using pytest May 06, 2019 N/A N/A - :pypi:`pytest-click` Py.test plugin for Click Aug 29, 2020 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-clld` Nov 29, 2021 N/A pytest (>=3.6) - :pypi:`pytest-cloud` Distributed tests planner plugin for pytest testing framework. Oct 05, 2020 6 - Mature N/A - :pypi:`pytest-cloudflare-worker` pytest plugin for testing cloudflare workers Mar 30, 2021 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-cobra` PyTest plugin for testing Smart Contracts for Ethereum blockchain. Jun 29, 2019 3 - Alpha pytest (<4.0.0,>=3.7.1) - :pypi:`pytest-codeblocks` Test code blocks in your READMEs Oct 13, 2021 4 - Beta pytest (>=6) - :pypi:`pytest-codecheckers` pytest plugin to add source code sanity checks (pep8 and friends) Feb 13, 2010 N/A N/A - :pypi:`pytest-codecov` Pytest plugin for uploading pytest-cov results to codecov.io Oct 27, 2021 4 - Beta pytest (>=4.6.0) - :pypi:`pytest-codegen` Automatically create pytest test signatures Aug 23, 2020 2 - Pre-Alpha N/A - :pypi:`pytest-codestyle` pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A - :pypi:`pytest-collect-formatter` Formatter for pytest collect output Mar 29, 2021 5 - Production/Stable N/A - :pypi:`pytest-collect-formatter2` Formatter for pytest collect output May 31, 2021 5 - Production/Stable N/A - :pypi:`pytest-colordots` Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A - :pypi:`pytest-commander` An interactive GUI test runner for PyTest Aug 17, 2021 N/A pytest (<7.0.0,>=6.2.4) - :pypi:`pytest-common-subject` pytest framework for testing different aspects of a common method Nov 12, 2020 N/A pytest (>=3.6,<7) - :pypi:`pytest-concurrent` Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-config` Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A - :pypi:`pytest-confluence-report` Package stands for pytest plugin to upload results into Confluence page. Nov 06, 2020 N/A N/A - :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts Sep 28, 2021 4 - Beta N/A - :pypi:`pytest-consul` pytest plugin with fixtures for testing consul aware apps Nov 24, 2018 3 - Alpha pytest - :pypi:`pytest-container` Pytest fixtures for writing container based tests Nov 19, 2021 3 - Alpha pytest (>=3.10) - :pypi:`pytest-contextfixture` Define pytest fixtures as context managers. Mar 12, 2013 4 - Beta N/A - :pypi:`pytest-contexts` A plugin to run tests written with the Contexts framework using pytest May 19, 2021 4 - Beta N/A - :pypi:`pytest-cookies` The pytest plugin for your Cookiecutter templates. 🍪 May 24, 2021 5 - Production/Stable pytest (>=3.3.0) - :pypi:`pytest-couchdbkit` py.test extension for per-test couchdb databases using couchdbkit Apr 17, 2012 N/A N/A - :pypi:`pytest-count` count erros and send email Jan 12, 2018 4 - Beta N/A - :pypi:`pytest-cov` Pytest plugin for measuring coverage. Oct 04, 2021 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-cover` Pytest plugin for measuring coverage. Forked from \`pytest-cov\`. Aug 01, 2015 5 - Production/Stable N/A - :pypi:`pytest-coverage` Jun 17, 2015 N/A N/A - :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jan 04, 2021 4 - Beta pytest (>=6.1.0) - :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' - :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Dec 03, 2021 5 - Production/Stable pytest (!=5.4.0,!=5.4.1) - :pypi:`pytest-cram` Run cram tests with pytest. Aug 08, 2020 N/A N/A - :pypi:`pytest-crate` Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) - :pypi:`pytest-cricri` A Cricri plugin for pytest. Jan 27, 2018 N/A pytest - :pypi:`pytest-crontab` add crontab task in crontab Dec 09, 2019 N/A N/A - :pypi:`pytest-csv` CSV output for pytest. Apr 22, 2021 N/A pytest (>=6.0) - :pypi:`pytest-curio` Pytest support for curio. Oct 07, 2020 N/A N/A - :pypi:`pytest-curl-report` pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A - :pypi:`pytest-custom-concurrency` Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A - :pypi:`pytest-custom-exit-code` Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) - :pypi:`pytest-custom-nodeid` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 07, 2021 N/A N/A - :pypi:`pytest-custom-report` Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest - :pypi:`pytest-custom-scheduling` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 01, 2021 N/A N/A - :pypi:`pytest-cython` A plugin for testing Cython extension modules Jan 26, 2021 4 - Beta pytest (>=2.7.3) - :pypi:`pytest-darker` A pytest plugin for checking of modified code using Darker Aug 16, 2020 N/A pytest (>=6.0.1) ; extra == 'test' - :pypi:`pytest-dash` pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A - :pypi:`pytest-data` Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A - :pypi:`pytest-databricks` Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest - :pypi:`pytest-datadir` pytest plugin for test data directories and files Oct 22, 2019 5 - Production/Stable pytest (>=2.7.0) - :pypi:`pytest-datadir-mgr` Manager for test data providing downloads, caching of generated files, and a context for temp directories. Aug 16, 2021 5 - Production/Stable pytest - :pypi:`pytest-datadir-ng` Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. Dec 25, 2019 5 - Production/Stable pytest - :pypi:`pytest-data-file` Fixture "data" and "case_data" for test from yaml file Dec 04, 2019 N/A N/A - :pypi:`pytest-datafiles` py.test plugin to create a 'tmpdir' containing predefined files/directories. Oct 07, 2018 5 - Production/Stable pytest (>=3.6) - :pypi:`pytest-datafixtures` Data fixtures for pytest made simple Dec 05, 2020 5 - Production/Stable N/A - :pypi:`pytest-data-from-files` pytest plugin to provide data from files loaded automatically Oct 13, 2021 4 - Beta pytest - :pypi:`pytest-dataplugin` A pytest plugin for managing an archive of test data. Sep 16, 2017 1 - Planning N/A - :pypi:`pytest-datarecorder` A py.test plugin recording and comparing test output. Apr 20, 2020 5 - Production/Stable pytest - :pypi:`pytest-datatest` A pytest plugin for test driven data-wrangling (this is the development version of datatest's pytest integration). Oct 15, 2020 4 - Beta pytest (>=3.3) - :pypi:`pytest-db` Session scope fixture "db" for mysql query or change Dec 04, 2019 N/A N/A - :pypi:`pytest-dbfixtures` Databases fixtures plugin for py.test. Dec 07, 2016 4 - Beta N/A - :pypi:`pytest-db-plugin` Nov 27, 2021 N/A pytest (>=5.0) - :pypi:`pytest-dbt-adapter` A pytest plugin for testing dbt adapter plugins Nov 24, 2021 N/A pytest (<7,>=6) - :pypi:`pytest-dbus-notification` D-BUS notifications for pytest results. Mar 05, 2014 5 - Production/Stable N/A - :pypi:`pytest-deadfixtures` A simple plugin to list unused fixtures in pytest Jul 23, 2020 5 - Production/Stable N/A - :pypi:`pytest-deepcov` deepcov Mar 30, 2021 N/A N/A - :pypi:`pytest-defer` Aug 24, 2021 N/A N/A - :pypi:`pytest-demo-plugin` pytest示例插件 May 15, 2021 N/A N/A - :pypi:`pytest-dependency` Manage dependencies of tests Feb 14, 2020 4 - Beta N/A - :pypi:`pytest-depends` Tests that depend on other tests Apr 05, 2020 5 - Production/Stable pytest (>=3) - :pypi:`pytest-deprecate` Mark tests as testing a deprecated feature with a warning note. Jul 01, 2019 N/A N/A - :pypi:`pytest-describe` Describe-style plugin for pytest Nov 13, 2021 4 - Beta pytest (>=4.0.0) - :pypi:`pytest-describe-it` plugin for rich text descriptions Jul 19, 2019 4 - Beta pytest - :pypi:`pytest-devpi-server` DevPI server fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-diamond` pytest plugin for diamond Aug 31, 2015 4 - Beta N/A - :pypi:`pytest-dicom` pytest plugin to provide DICOM fixtures Dec 19, 2018 3 - Alpha pytest - :pypi:`pytest-dictsdiff` Jul 26, 2019 N/A N/A - :pypi:`pytest-diff` A simple plugin to use with pytest Mar 30, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-disable` pytest plugin to disable a test and skip it from testrun Sep 10, 2015 4 - Beta N/A - :pypi:`pytest-disable-plugin` Disable plugins per test Feb 28, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-discord` A pytest plugin to notify test results to a Discord channel. Mar 20, 2021 3 - Alpha pytest (!=6.0.0,<7,>=3.3.2) - :pypi:`pytest-django` A Django plugin for pytest. Dec 02, 2021 5 - Production/Stable pytest (>=5.4.0) - :pypi:`pytest-django-ahead` A Django plugin for pytest. Oct 27, 2016 5 - Production/Stable pytest (>=2.9) - :pypi:`pytest-djangoapp` Nice pytest plugin to help you with Django pluggable application testing. Aug 04, 2021 4 - Beta N/A - :pypi:`pytest-django-cache-xdist` A djangocachexdist plugin for pytest May 12, 2020 4 - Beta N/A - :pypi:`pytest-django-casperjs` Integrate CasperJS with your django tests as a pytest fixture. Mar 15, 2015 2 - Pre-Alpha N/A - :pypi:`pytest-django-dotenv` Pytest plugin used to setup environment variables with django-dotenv Nov 26, 2019 4 - Beta pytest (>=2.6.0) - :pypi:`pytest-django-factories` Factories for your Django models that can be used as Pytest fixtures. Nov 12, 2020 4 - Beta N/A - :pypi:`pytest-django-gcir` A Django plugin for pytest. Mar 06, 2018 5 - Production/Stable N/A - :pypi:`pytest-django-haystack` Cleanup your Haystack indexes between tests Sep 03, 2017 5 - Production/Stable pytest (>=2.3.4) - :pypi:`pytest-django-ifactory` A model instance factory for pytest-django Jan 13, 2021 3 - Alpha N/A - :pypi:`pytest-django-lite` The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A - :pypi:`pytest-django-liveserver-ssl` Jul 30, 2021 3 - Alpha N/A - :pypi:`pytest-django-model` A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A - :pypi:`pytest-django-ordering` A pytest plugin for preserving the order in which Django runs tests. Jul 25, 2019 5 - Production/Stable pytest (>=2.3.0) - :pypi:`pytest-django-queries` Generate performance reports from your django database performance tests. Mar 01, 2021 N/A N/A - :pypi:`pytest-djangorestframework` A djangorestframework plugin for pytest Aug 11, 2019 4 - Beta N/A - :pypi:`pytest-django-rq` A pytest plugin to help writing unit test for django-rq Apr 13, 2020 4 - Beta N/A - :pypi:`pytest-django-sqlcounts` py.test plugin for reporting the number of SQLs executed per django testcase. Jun 16, 2015 4 - Beta N/A - :pypi:`pytest-django-testing-postgresql` Use a temporary PostgreSQL database with pytest-django Dec 05, 2019 3 - Alpha N/A - :pypi:`pytest-doc` A documentation plugin for py.test. Jun 28, 2015 5 - Production/Stable N/A - :pypi:`pytest-docgen` An RST Documentation Generator for pytest-based test suites Apr 17, 2020 N/A N/A - :pypi:`pytest-docker` Simple pytest fixtures for Docker and docker-compose based tests Jun 14, 2021 N/A pytest (<7.0,>=4.0) - :pypi:`pytest-docker-butla` Jun 16, 2019 3 - Alpha N/A - :pypi:`pytest-dockerc` Run, manage and stop Docker Compose project from Docker API Oct 09, 2020 5 - Production/Stable pytest (>=3.0) - :pypi:`pytest-docker-compose` Manages Docker containers during your integration tests Jan 26, 2021 5 - Production/Stable pytest (>=3.3) - :pypi:`pytest-docker-db` A plugin to use docker databases for pytests Mar 20, 2021 5 - Production/Stable pytest (>=3.1.1) - :pypi:`pytest-docker-fixtures` pytest docker fixtures Nov 23, 2021 3 - Alpha N/A - :pypi:`pytest-docker-git-fixtures` Pytest fixtures for testing with git scm. Mar 11, 2021 4 - Beta pytest - :pypi:`pytest-docker-pexpect` pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest - :pypi:`pytest-docker-postgresql` A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-docker-py` Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) - :pypi:`pytest-docker-registry-fixtures` Pytest fixtures for testing with docker registries. Mar 04, 2021 4 - Beta pytest - :pypi:`pytest-docker-tools` Docker integration tests for pytest Jul 23, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) - :pypi:`pytest-docs` Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-docstyle` pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A - :pypi:`pytest-doctest-custom` A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A - :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A - :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) - :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Nov 16, 2021 3 - Alpha pytest (>=4.6) - :pypi:`pytest-doctest-ufunc` A plugin to run doctests in docstrings of Numpy ufuncs Aug 02, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) - :pypi:`pytest-doorstop` A pytest plugin for adding test results into doorstop items. Jun 09, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-dotenv` A py.test plugin that parses environment files before running tests Jun 16, 2020 4 - Beta pytest (>=5.0.0) - :pypi:`pytest-drf` A Django REST framework plugin for pytest. Nov 12, 2020 5 - Production/Stable pytest (>=3.6) - :pypi:`pytest-drivings` Tool to allow webdriver automation to be ran locally or remotely Jan 13, 2021 N/A N/A - :pypi:`pytest-drop-dup-tests` A Pytest plugin to drop duplicated tests during collection May 23, 2020 4 - Beta pytest (>=2.7) - :pypi:`pytest-dummynet` A py.test plugin providing access to a dummynet. Oct 13, 2021 5 - Production/Stable pytest - :pypi:`pytest-dump2json` A pytest plugin for dumping test results to json. Jun 29, 2015 N/A N/A - :pypi:`pytest-duration-insights` Jun 25, 2021 N/A N/A - :pypi:`pytest-dynamicrerun` A pytest plugin to rerun tests dynamically based off of test outcome and output. Aug 15, 2020 4 - Beta N/A - :pypi:`pytest-dynamodb` DynamoDB fixtures for pytest Jun 03, 2021 5 - Production/Stable pytest - :pypi:`pytest-easy-addoption` pytest-easy-addoption: Easy way to work with pytest addoption Jan 22, 2020 N/A N/A - :pypi:`pytest-easy-api` Simple API testing with pytest Mar 26, 2018 N/A N/A - :pypi:`pytest-easyMPI` Package that supports mpi tests in pytest Oct 21, 2020 N/A N/A - :pypi:`pytest-easyread` pytest plugin that makes terminal printouts of the reports easier to read Nov 17, 2017 N/A N/A - :pypi:`pytest-easy-server` Pytest plugin for easy testing against servers May 01, 2021 4 - Beta pytest (<5.0.0,>=4.3.1) ; python_version < "3.5" - :pypi:`pytest-ec2` Pytest execution on EC2 instance Oct 22, 2019 3 - Alpha N/A - :pypi:`pytest-echo` pytest plugin with mechanisms for echoing environment variables, package version and generic attributes Jan 08, 2020 5 - Production/Stable N/A - :pypi:`pytest-elasticsearch` Elasticsearch fixtures and fixture factories for Pytest. May 12, 2021 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-elements` Tool to help automate user interfaces Jan 13, 2021 N/A pytest (>=5.4,<6.0) - :pypi:`pytest-elk-reporter` A simple plugin to use with pytest Jan 24, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest - :pypi:`pytest-embedded` pytest embedded plugin Nov 29, 2021 N/A pytest (>=6.2.0) - :pypi:`pytest-embedded-idf` pytest embedded plugin for esp-idf project Nov 29, 2021 N/A N/A - :pypi:`pytest-embedded-jtag` pytest embedded plugin for testing with jtag Nov 29, 2021 N/A N/A - :pypi:`pytest-embedded-qemu` pytest embedded plugin for qemu, not target chip Nov 29, 2021 N/A N/A - :pypi:`pytest-embedded-qemu-idf` pytest embedded plugin for esp-idf project by qemu, not target chip Jun 29, 2021 N/A N/A - :pypi:`pytest-embedded-serial` pytest embedded plugin for testing serial ports Nov 29, 2021 N/A N/A - :pypi:`pytest-embedded-serial-esp` pytest embedded plugin for testing espressif boards via serial ports Nov 29, 2021 N/A N/A - :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) - :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Oct 10, 2021 4 - Beta pytest (==6.0.1) - :pypi:`pytest-enabler` Enable installed pytest plugins Nov 08, 2021 5 - Production/Stable pytest (>=6) ; extra == 'testing' - :pypi:`pytest-encode` set your encoding and logger Nov 06, 2021 N/A N/A - :pypi:`pytest-encode-kane` set your encoding and logger Nov 16, 2021 N/A pytest - :pypi:`pytest-enhancements` Improvements for pytest (rejected upstream) Oct 30, 2019 4 - Beta N/A - :pypi:`pytest-env` py.test plugin that allows you to add environment variables. Jun 16, 2017 4 - Beta N/A - :pypi:`pytest-envfiles` A py.test plugin that parses environment files before running tests Oct 08, 2015 3 - Alpha N/A - :pypi:`pytest-env-info` Push information about the running pytest into envvars Nov 25, 2017 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-envraw` py.test plugin that allows you to add environment variables. Aug 27, 2020 4 - Beta pytest (>=2.6.0) - :pypi:`pytest-envvars` Pytest plugin to validate use of envvars on your tests Jun 13, 2020 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-env-yaml` Apr 02, 2019 N/A N/A - :pypi:`pytest-eradicate` pytest plugin to check for commented out code Sep 08, 2020 N/A pytest (>=2.4.2) - :pypi:`pytest-error-for-skips` Pytest plugin to treat skipped tests a test failure Dec 19, 2019 4 - Beta pytest (>=4.6) - :pypi:`pytest-eth` PyTest plugin for testing Smart Contracts for Ethereum Virtual Machine (EVM). Aug 14, 2020 1 - Planning N/A - :pypi:`pytest-ethereum` pytest-ethereum: Pytest library for ethereum projects. Jun 24, 2019 3 - Alpha pytest (==3.3.2); extra == 'dev' - :pypi:`pytest-eucalyptus` Pytest Plugin for BDD Aug 13, 2019 N/A pytest (>=4.2.0) - :pypi:`pytest-eventlet` Applies eventlet monkey-patch as a pytest plugin. Oct 04, 2021 N/A pytest ; extra == 'dev' - :pypi:`pytest-excel` pytest plugin for generating excel reports Oct 06, 2020 5 - Production/Stable N/A - :pypi:`pytest-exceptional` Better exceptions Mar 16, 2017 4 - Beta N/A - :pypi:`pytest-exception-script` Walk your code through exception script to check it's resiliency to failures. Aug 04, 2020 3 - Alpha pytest - :pypi:`pytest-executable` pytest plugin for testing executables Nov 10, 2021 4 - Beta pytest (<6.3,>=4.3) - :pypi:`pytest-expect` py.test plugin to store test expectations and mark tests based on them Apr 21, 2016 4 - Beta N/A - :pypi:`pytest-expecter` Better testing with expecter and pytest. Jul 08, 2020 5 - Production/Stable N/A - :pypi:`pytest-expectr` This plugin is used to expect multiple assert using pytest framework. Oct 05, 2018 N/A pytest (>=2.4.2) - :pypi:`pytest-explicit` A Pytest plugin to ignore certain marked tests by default Jun 15, 2021 5 - Production/Stable pytest - :pypi:`pytest-exploratory` Interactive console for pytest. Aug 03, 2021 N/A pytest (>=5.3) - :pypi:`pytest-external-blockers` a special outcome for tests that are blocked for external reasons Oct 05, 2021 N/A pytest - :pypi:`pytest-extra-durations` A pytest plugin to get durations on a per-function basis and per module basis. Apr 21, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-fabric` Provides test utilities to run fabric task tests by using docker containers Sep 12, 2018 5 - Production/Stable N/A - :pypi:`pytest-factory` Use factories for test setup with py.test Sep 06, 2020 3 - Alpha pytest (>4.3) - :pypi:`pytest-factoryboy` Factory Boy support for pytest. Dec 30, 2020 6 - Mature pytest (>=4.6) - :pypi:`pytest-factoryboy-fixtures` Generates pytest fixtures that allow the use of type hinting Jun 25, 2020 N/A N/A - :pypi:`pytest-factoryboy-state` Simple factoryboy random state management Dec 11, 2020 4 - Beta pytest (>=5.0) - :pypi:`pytest-failed-screenshot` Test case fails,take a screenshot,save it,attach it to the allure Apr 21, 2021 N/A N/A - :pypi:`pytest-failed-to-verify` A pytest plugin that helps better distinguishing real test failures from setup flakiness. Aug 08, 2019 5 - Production/Stable pytest (>=4.1.0) - :pypi:`pytest-faker` Faker integration with the pytest framework. Dec 19, 2016 6 - Mature N/A - :pypi:`pytest-falcon` Pytest helpers for Falcon. Sep 07, 2016 4 - Beta N/A - :pypi:`pytest-falcon-client` Pytest \`client\` fixture for the Falcon Framework Mar 19, 2019 N/A N/A - :pypi:`pytest-fantasy` Pytest plugin for Flask Fantasy Framework Mar 14, 2019 N/A N/A - :pypi:`pytest-fastapi` Dec 27, 2020 N/A N/A - :pypi:`pytest-fastest` Use SCM and coverage to run only needed tests Mar 05, 2020 N/A N/A - :pypi:`pytest-fast-first` Pytest plugin that runs fast tests first Apr 02, 2021 3 - Alpha pytest - :pypi:`pytest-faulthandler` py.test plugin that activates the fault handler module for tests (dummy package) Jul 04, 2019 6 - Mature pytest (>=5.0) - :pypi:`pytest-fauxfactory` Integration of fauxfactory into pytest. Dec 06, 2017 5 - Production/Stable pytest (>=3.2) - :pypi:`pytest-figleaf` py.test figleaf coverage plugin Jan 18, 2010 5 - Production/Stable N/A - :pypi:`pytest-filecov` A pytest plugin to detect unused files Jun 27, 2021 4 - Beta pytest - :pypi:`pytest-filedata` easily load data from files Jan 17, 2019 4 - Beta N/A - :pypi:`pytest-filemarker` A pytest plugin that runs marked tests when files change. Dec 01, 2020 N/A pytest - :pypi:`pytest-filter-case` run test cases filter by mark Nov 05, 2020 N/A N/A - :pypi:`pytest-filter-subpackage` Pytest plugin for filtering based on sub-packages Jan 09, 2020 3 - Alpha pytest (>=3.0) - :pypi:`pytest-find-dependencies` A pytest plugin to find dependencies between tests Apr 21, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-finer-verdicts` A pytest plugin to treat non-assertion failures as test errors. Jun 18, 2020 N/A pytest (>=5.4.3) - :pypi:`pytest-firefox` pytest plugin to manipulate firefox Aug 08, 2017 3 - Alpha pytest (>=3.0.2) - :pypi:`pytest-fixture-config` Fixture configuration utils for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-fixture-maker` Pytest plugin to load fixtures from YAML files Sep 21, 2021 N/A N/A - :pypi:`pytest-fixture-marker` A pytest plugin to add markers based on fixtures used. Oct 11, 2020 5 - Production/Stable N/A - :pypi:`pytest-fixture-order` pytest plugin to control fixture evaluation order Aug 25, 2020 N/A pytest (>=3.0) - :pypi:`pytest-fixtures` Common fixtures for pytest May 01, 2019 5 - Production/Stable N/A - :pypi:`pytest-fixture-tools` Plugin for pytest which provides tools for fixtures Aug 18, 2020 6 - Mature pytest - :pypi:`pytest-fixture-typecheck` A pytest plugin to assert type annotations at runtime. Aug 24, 2021 N/A pytest - :pypi:`pytest-flake8` pytest plugin to check FLAKE8 requirements Dec 16, 2020 4 - Beta pytest (>=3.5) - :pypi:`pytest-flake8-path` A pytest fixture for testing flake8 plugins. Aug 11, 2021 5 - Production/Stable pytest - :pypi:`pytest-flakefinder` Runs tests multiple times to expose flakiness. Jul 28, 2020 4 - Beta pytest (>=2.7.1) - :pypi:`pytest-flakes` pytest plugin to check source code with pyflakes Dec 02, 2021 5 - Production/Stable pytest (>=5) - :pypi:`pytest-flaptastic` Flaptastic py.test plugin Mar 17, 2019 N/A N/A - :pypi:`pytest-flask` A set of py.test fixtures to test Flask applications. Feb 27, 2021 5 - Production/Stable pytest (>=5.2) - :pypi:`pytest-flask-sqlalchemy` A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 04, 2019 4 - Beta pytest (>=3.2.1) - :pypi:`pytest-flask-sqlalchemy-transactions` Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) - :pypi:`pytest-flyte` Pytest fixtures for simplifying Flyte integration testing May 03, 2021 N/A pytest - :pypi:`pytest-focus` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest - :pypi:`pytest-forcefail` py.test plugin to make the test failing regardless of pytest.mark.xfail May 15, 2018 4 - Beta N/A - :pypi:`pytest-forward-compatability` A name to avoid typosquating pytest-foward-compatibility Sep 06, 2020 N/A N/A - :pypi:`pytest-forward-compatibility` A pytest plugin to shim pytest commandline options for fowards compatibility Sep 29, 2020 N/A N/A - :pypi:`pytest-freezegun` Wrap tests with fixtures in freeze_time Jul 19, 2020 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-freeze-reqs` Check if requirement files are frozen Apr 29, 2021 N/A N/A - :pypi:`pytest-frozen-uuids` Deterministically frozen UUID's for your tests Oct 19, 2021 N/A pytest (>=3.0) - :pypi:`pytest-func-cov` Pytest plugin for measuring function coverage Apr 15, 2021 3 - Alpha pytest (>=5) - :pypi:`pytest-funparam` An alternative way to parametrize test cases. Dec 02, 2021 4 - Beta pytest >=4.6.0 - :pypi:`pytest-fxa` pytest plugin for Firefox Accounts Aug 28, 2018 5 - Production/Stable N/A - :pypi:`pytest-fxtest` Oct 27, 2020 N/A N/A - :pypi:`pytest-gc` The garbage collector plugin for py.test Feb 01, 2018 N/A N/A - :pypi:`pytest-gcov` Uses gcov to measure test coverage of a C library Feb 01, 2018 3 - Alpha N/A - :pypi:`pytest-gevent` Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest - :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) - :pypi:`pytest-ghostinspector` For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A - :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Nov 30, 2021 N/A N/A - :pypi:`pytest-git` Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-gitcov` Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A - :pypi:`pytest-git-fixtures` Pytest fixtures for testing with git. Mar 11, 2021 4 - Beta pytest - :pypi:`pytest-github` Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A - :pypi:`pytest-github-actions-annotate-failures` pytest plugin to annotate failed tests with a workflow command for GitHub Actions Oct 24, 2021 N/A pytest (>=4.0.0) - :pypi:`pytest-gitignore` py.test plugin to ignore the same files as git Jul 17, 2015 4 - Beta N/A - :pypi:`pytest-glamor-allure` Extends allure-pytest functionality Nov 26, 2021 4 - Beta pytest - :pypi:`pytest-gnupg-fixtures` Pytest fixtures for testing with gnupg. Mar 04, 2021 4 - Beta pytest - :pypi:`pytest-golden` Plugin for pytest that offloads expected outputs to data files Nov 23, 2020 N/A pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-graphql-schema` Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A - :pypi:`pytest-greendots` Green progress dots Feb 08, 2014 3 - Alpha N/A - :pypi:`pytest-growl` Growl notifications for pytest results. Jan 13, 2014 5 - Production/Stable N/A - :pypi:`pytest-grpc` pytest plugin for grpc May 01, 2020 N/A pytest (>=3.6.0) - :pypi:`pytest-hammertime` Display "🔨 " instead of "." for passed pytest tests. Jul 28, 2018 N/A pytest - :pypi:`pytest-harvest` Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes. Apr 01, 2021 5 - Production/Stable N/A - :pypi:`pytest-helm-chart` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Jun 15, 2020 4 - Beta pytest (>=5.4.2,<6.0.0) - :pypi:`pytest-helm-charts` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Oct 26, 2021 4 - Beta pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-helper` Functions to help in using the pytest testing framework May 31, 2019 5 - Production/Stable N/A - :pypi:`pytest-helpers` pytest helpers May 17, 2020 N/A pytest - :pypi:`pytest-helpers-namespace` Pytest Helpers Namespace Plugin Apr 29, 2021 5 - Production/Stable pytest (>=6.0.0) - :pypi:`pytest-hidecaptured` Hide captured output May 04, 2018 4 - Beta pytest (>=2.8.5) - :pypi:`pytest-historic` Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest - :pypi:`pytest-historic-hook` Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest - :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Nov 20, 2021 3 - Alpha pytest (==6.2.5) - :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A - :pypi:`pytest-hoverfly` Simplify working with Hoverfly from pytest Jul 12, 2021 N/A pytest (>=5.0) - :pypi:`pytest-hoverfly-wrapper` Integrates the Hoverfly HTTP proxy into Pytest Aug 29, 2021 4 - Beta N/A - :pypi:`pytest-hpfeeds` Helpers for testing hpfeeds in your python project Aug 27, 2021 4 - Beta pytest (>=6.2.4,<7.0.0) - :pypi:`pytest-html` pytest plugin for generating HTML reports Dec 13, 2020 5 - Production/Stable pytest (!=6.0.0,>=5.0) - :pypi:`pytest-html-lee` optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-html-profiling` Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) - :pypi:`pytest-html-reporter` Generates a static html report based on pytest framework Apr 25, 2021 N/A N/A - :pypi:`pytest-html-thread` pytest plugin for generating HTML reports Dec 29, 2020 5 - Production/Stable N/A - :pypi:`pytest-http` Fixture "http" for http requests Dec 05, 2019 N/A N/A - :pypi:`pytest-httpbin` Easily test your HTTP library against a local copy of httpbin Feb 11, 2019 5 - Production/Stable N/A - :pypi:`pytest-http-mocker` Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A - :pypi:`pytest-httpretty` A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A - :pypi:`pytest-httpserver` pytest-httpserver is a httpserver for pytest Oct 18, 2021 3 - Alpha pytest ; extra == 'dev' - :pypi:`pytest-httpx` Send responses to httpx. Nov 16, 2021 5 - Production/Stable pytest (==6.*) - :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Nov 16, 2021 N/A pytest (>=6.2.5) - :pypi:`pytest-hue` Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A - :pypi:`pytest-hylang` Pytest plugin to allow running tests written in hylang Mar 28, 2021 N/A pytest - :pypi:`pytest-hypo-25` help hypo module for pytest Jan 12, 2020 3 - Alpha N/A - :pypi:`pytest-ibutsu` A plugin to sent pytest results to an Ibutsu server Jun 16, 2021 4 - Beta pytest - :pypi:`pytest-icdiff` use icdiff for better error messages in pytest assertions Apr 08, 2020 4 - Beta N/A - :pypi:`pytest-idapro` A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A - :pypi:`pytest-idempotent` Pytest plugin for testing function idempotence. Nov 26, 2021 N/A N/A - :pypi:`pytest-ignore-flaky` ignore failures from flaky tests (pytest plugin) Apr 23, 2021 5 - Production/Stable N/A - :pypi:`pytest-image-diff` Jul 28, 2021 3 - Alpha pytest - :pypi:`pytest-incremental` an incremental test runner (pytest plugin) Apr 24, 2021 5 - Production/Stable N/A - :pypi:`pytest-influxdb` Plugin for influxdb and pytest integration. Apr 20, 2021 N/A N/A - :pypi:`pytest-info-collector` pytest plugin to collect information from tests May 26, 2019 3 - Alpha N/A - :pypi:`pytest-informative-node` display more node ininformation. Apr 25, 2019 4 - Beta N/A - :pypi:`pytest-infrastructure` pytest stack validation prior to testing executing Apr 12, 2020 4 - Beta N/A - :pypi:`pytest-ini` Reuse pytest.ini to store env variables Sep 30, 2021 N/A N/A - :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Aug 17, 2021 5 - Production/Stable N/A - :pypi:`pytest-inmanta-extensions` Inmanta tests package May 27, 2021 5 - Production/Stable N/A - :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A - :pypi:`pytest-insta` A practical snapshot testing plugin for pytest Apr 07, 2021 N/A pytest (>=6.0.2,<7.0.0) - :pypi:`pytest-instafail` pytest plugin to show failures instantly Jun 14, 2020 4 - Beta pytest (>=2.9) - :pypi:`pytest-instrument` pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) - :pypi:`pytest-integration` Organizing pytests by integration or not Apr 16, 2020 N/A N/A - :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest Jul 19, 2021 N/A pytest (>=5.2,<7.0) - :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A - :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) - :pypi:`pytest-invenio` Pytest fixtures for Invenio. May 11, 2021 5 - Production/Stable pytest (<7,>=6) - :pypi:`pytest-involve` Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Sep 02, 2014 2 - Pre-Alpha N/A - :pypi:`pytest-ipynb` THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A - :pypi:`pytest-isort` py.test plugin to check import ordering using isort Apr 27, 2021 5 - Production/Stable N/A - :pypi:`pytest-it` Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 22, 2020 4 - Beta N/A - :pypi:`pytest-iterassert` Nicer list and iterable assertion messages for pytest May 11, 2020 3 - Alpha N/A - :pypi:`pytest-jasmine` Run jasmine tests from your pytest test suite Nov 04, 2017 1 - Planning N/A - :pypi:`pytest-jest` A custom jest-pytest oriented Pytest reporter May 22, 2018 4 - Beta pytest (>=3.3.2) - :pypi:`pytest-jira` py.test JIRA integration plugin, using markers Dec 02, 2021 3 - Alpha N/A - :pypi:`pytest-jira-xray` pytest plugin to integrate tests with JIRA XRAY Nov 28, 2021 3 - Alpha pytest - :pypi:`pytest-jobserver` Limit parallel tests with posix jobserver. May 15, 2019 5 - Production/Stable pytest - :pypi:`pytest-joke` Test failures are better served with humor. Oct 08, 2019 4 - Beta pytest (>=4.2.1) - :pypi:`pytest-json` Generate JSON test reports Jan 18, 2016 4 - Beta N/A - :pypi:`pytest-jsonlint` UNKNOWN Aug 04, 2016 N/A N/A - :pypi:`pytest-json-report` A pytest plugin to report test results as JSON files Sep 24, 2021 4 - Beta pytest (>=3.8.0) - :pypi:`pytest-kafka` Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest Aug 24, 2021 N/A pytest - :pypi:`pytest-kafkavents` A plugin to send pytest events to Kafka Sep 08, 2021 4 - Beta pytest - :pypi:`pytest-kind` Kubernetes test support with KIND for pytest Jan 24, 2021 5 - Production/Stable N/A - :pypi:`pytest-kivy` Kivy GUI tests fixtures using pytest Jul 06, 2021 4 - Beta pytest (>=3.6) - :pypi:`pytest-knows` A pytest plugin that can automaticly skip test case based on dependence info calculated by trace Aug 22, 2014 N/A N/A - :pypi:`pytest-konira` Run Konira DSL tests with py.test Oct 09, 2011 N/A N/A - :pypi:`pytest-krtech-common` pytest krtech common library Nov 28, 2016 4 - Beta N/A - :pypi:`pytest-kwparametrize` Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks Jan 22, 2021 N/A pytest (>=6) - :pypi:`pytest-lambda` Define pytest fixtures with lambda functions. Aug 23, 2021 3 - Alpha pytest (>=3.6,<7) - :pypi:`pytest-lamp` Jan 06, 2017 3 - Alpha N/A - :pypi:`pytest-layab` Pytest fixtures for layab. Oct 05, 2020 5 - Production/Stable N/A - :pypi:`pytest-lazy-fixture` It helps to use fixtures in pytest.mark.parametrize Feb 01, 2020 4 - Beta pytest (>=3.2.5) - :pypi:`pytest-ldap` python-ldap fixtures for pytest Aug 18, 2020 N/A pytest - :pypi:`pytest-leaks` A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A - :pypi:`pytest-level` Select tests of a given level or lower Oct 21, 2019 N/A pytest - :pypi:`pytest-libfaketime` A python-libfaketime plugin for pytest. Dec 22, 2018 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-libiio` A pytest plugin to manage interfacing with libiio contexts Oct 29, 2021 4 - Beta N/A - :pypi:`pytest-libnotify` Pytest plugin that shows notifications about the test run Apr 02, 2021 3 - Alpha pytest - :pypi:`pytest-ligo` Jan 16, 2020 4 - Beta N/A - :pypi:`pytest-lineno` A pytest plugin to show the line numbers of test functions Dec 04, 2020 N/A pytest - :pypi:`pytest-line-profiler` Profile code executed by pytest May 03, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-lisa` Pytest plugin for organizing tests. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-listener` A simple network listener May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-litf` A pytest plugin that stream output in LITF format Jan 18, 2021 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-live` Live results for pytest Mar 08, 2020 N/A pytest - :pypi:`pytest-localftpserver` A PyTest plugin which provides an FTP fixture for your tests Aug 25, 2021 5 - Production/Stable pytest - :pypi:`pytest-localserver` py.test plugin to test server connections locally. Nov 19, 2021 4 - Beta N/A - :pypi:`pytest-localstack` Pytest plugin for AWS integration tests Aug 22, 2019 4 - Beta pytest (>=3.3.0) - :pypi:`pytest-lockable` lockable resource plugin for pytest Nov 09, 2021 5 - Production/Stable pytest - :pypi:`pytest-locker` Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Oct 29, 2021 N/A pytest (>=5.4) - :pypi:`pytest-log` print log Aug 15, 2021 N/A pytest (>=3.8) - :pypi:`pytest-logbook` py.test plugin to capture logbook log messages Nov 23, 2015 5 - Production/Stable pytest (>=2.8) - :pypi:`pytest-logdog` Pytest plugin to test logging Jun 15, 2021 1 - Planning pytest (>=6.2.0) - :pypi:`pytest-logfest` Pytest plugin providing three logger fixtures with basic or full writing to log files Jul 21, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-logger` Plugin configuring handlers for loggers from Python logging module. Jul 25, 2019 4 - Beta pytest (>=3.2) - :pypi:`pytest-logging` Configures logging and allows tweaking the log level with a py.test flag Nov 04, 2015 4 - Beta N/A - :pypi:`pytest-log-report` Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A - :pypi:`pytest-manual-marker` pytest marker for marking manual tests Oct 11, 2021 3 - Alpha pytest (>=6) - :pypi:`pytest-markdown` Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) - :pypi:`pytest-marker-bugzilla` py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A - :pypi:`pytest-markers-presence` A simple plugin to detect missed pytest tags and markers" Feb 04, 2021 4 - Beta pytest (>=6.0) - :pypi:`pytest-markfiltration` UNKNOWN Nov 08, 2011 3 - Alpha N/A - :pypi:`pytest-mark-no-py3` pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest - :pypi:`pytest-marks` UNKNOWN Nov 23, 2012 3 - Alpha N/A - :pypi:`pytest-matcher` Match test output against patterns stored in files Apr 23, 2020 5 - Production/Stable pytest (>=3.4) - :pypi:`pytest-match-skip` Skip matching marks. Matches partial marks using wildcards. May 15, 2019 4 - Beta pytest (>=4.4.1) - :pypi:`pytest-mat-report` this is report Jan 20, 2021 N/A N/A - :pypi:`pytest-matrix` Provide tools for generating tests from combinations of fixtures. Jun 24, 2020 5 - Production/Stable pytest (>=5.4.3,<6.0.0) - :pypi:`pytest-mccabe` pytest plugin to run the mccabe code complexity checker. Jul 22, 2020 3 - Alpha pytest (>=5.4.0) - :pypi:`pytest-md` Plugin for generating Markdown reports for pytest results Jul 11, 2019 3 - Alpha pytest (>=4.2.1) - :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. May 04, 2021 4 - Beta pytest (!=6.0.0,<7,>=3.3.2) - :pypi:`pytest-memprof` Estimates memory consumption of test functions Mar 29, 2019 4 - Beta N/A - :pypi:`pytest-menu` A pytest plugin for console based interactive test selection just after the collection phase Oct 04, 2017 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-mercurial` pytest plugin to write integration tests for projects using Mercurial Python internals Nov 21, 2020 1 - Planning N/A - :pypi:`pytest-message` Pytest plugin for sending report message of marked tests execution Nov 04, 2021 N/A pytest (>=6.2.5) - :pypi:`pytest-messenger` Pytest to Slack reporting plugin Dec 16, 2020 5 - Production/Stable N/A - :pypi:`pytest-metadata` pytest plugin for test session metadata Nov 27, 2020 5 - Production/Stable pytest (>=2.9.0) - :pypi:`pytest-metrics` Custom metrics report for pytest Apr 04, 2020 N/A pytest - :pypi:`pytest-mimesis` Mimesis integration with the pytest test runner Mar 21, 2020 5 - Production/Stable pytest (>=4.2) - :pypi:`pytest-minecraft` A pytest plugin for running tests against Minecraft releases Sep 26, 2020 N/A pytest (>=6.0.1,<7.0.0) - :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-ml` Test your machine learning! May 04, 2019 4 - Beta N/A - :pypi:`pytest-mocha` pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) - :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest May 06, 2021 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-mock-api` A mock API server with configurable routes and responses available as a fixture. Feb 13, 2019 1 - Planning pytest (>=4.0.0) - :pypi:`pytest-mock-generator` A pytest fixture wrapper for https://pypi.org/project/mock-generator Aug 10, 2021 5 - Production/Stable N/A - :pypi:`pytest-mock-helper` Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest - :pypi:`pytest-mockito` Base fixtures for mockito Jul 11, 2018 4 - Beta N/A - :pypi:`pytest-mockredis` An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A - :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Dec 03, 2021 N/A pytest (>=1.0) - :pypi:`pytest-mock-server` Mock server plugin for pytest Apr 06, 2020 4 - Beta N/A - :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) - :pypi:`pytest-modifyjunit` Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A - :pypi:`pytest-modifyscope` pytest plugin to modify fixture scope Apr 12, 2020 N/A pytest - :pypi:`pytest-molecule` PyTest Molecule Plugin :: discover and run molecule tests Oct 06, 2021 5 - Production/Stable N/A - :pypi:`pytest-mongo` MongoDB process and client fixtures plugin for Pytest. Jun 07, 2021 5 - Production/Stable pytest - :pypi:`pytest-mongodb` pytest plugin for MongoDB fixtures Dec 07, 2019 5 - Production/Stable pytest (>=2.5.2) - :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Aug 24, 2021 5 - Production/Stable pytest - :pypi:`pytest-monkeyplus` pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A - :pypi:`pytest-monkeytype` pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A - :pypi:`pytest-moto` Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A - :pypi:`pytest-motor` A pytest plugin for motor, the non-blocking MongoDB driver. Jul 21, 2021 3 - Alpha pytest - :pypi:`pytest-mp` A test batcher for multiprocessed Pytest runs May 23, 2018 4 - Beta pytest - :pypi:`pytest-mpi` pytest plugin to collect information from tests Mar 14, 2021 3 - Alpha pytest - :pypi:`pytest-mpl` pytest plugin to help with testing figures output from Matplotlib Jul 02, 2021 4 - Beta pytest - :pypi:`pytest-mproc` low-startup-overhead, scalable, distributed-testing pytest plugin Mar 07, 2021 4 - Beta pytest - :pypi:`pytest-multi-check` Pytest-плагин, реализует возможность мульти проверок и мягких проверок Jun 03, 2021 N/A pytest - :pypi:`pytest-multihost` Utility for writing multi-host tests for pytest Apr 07, 2020 4 - Beta N/A - :pypi:`pytest-multilog` Multi-process logs handling and other helpers for pytest Jun 10, 2021 N/A N/A - :pypi:`pytest-multithreading` a pytest plugin for th and concurrent testing Aug 12, 2021 N/A pytest (>=3.6) - :pypi:`pytest-mutagen` Add the mutation testing feature to pytest Jul 24, 2020 N/A pytest (>=5.4) - :pypi:`pytest-mypy` Mypy static type checker plugin for Pytest Mar 21, 2021 4 - Beta pytest (>=3.5) - :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" - :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Oct 19, 2021 3 - Alpha pytest (>=6.0.0) - :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A N/A - :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Jun 13, 2021 N/A pytest - :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Nov 22, 2021 5 - Production/Stable pytest - :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) - :pypi:`pytest-neo` pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Apr 23, 2019 3 - Alpha pytest (>=3.7.2) - :pypi:`pytest-network` A simple plugin to disable network on socket level. May 07, 2020 N/A N/A - :pypi:`pytest-never-sleep` pytest plugin helps to avoid adding tests without mock \`time.sleep\` May 05, 2021 3 - Alpha pytest (>=3.5.1) - :pypi:`pytest-nginx` nginx fixture for pytest Aug 12, 2017 5 - Production/Stable N/A - :pypi:`pytest-nginx-iplweb` nginx fixture for pytest - iplweb temporary fork Mar 01, 2019 5 - Production/Stable N/A - :pypi:`pytest-ngrok` Jan 22, 2020 3 - Alpha N/A - :pypi:`pytest-ngsfixtures` pytest ngs fixtures Sep 06, 2019 2 - Pre-Alpha pytest (>=5.0.0) - :pypi:`pytest-nice` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest - :pypi:`pytest-nice-parametrize` A small snippet for nicer PyTest's Parametrize Apr 17, 2021 5 - Production/Stable N/A - :pypi:`pytest-nlcov` Pytest plugin to get the coverage of the new lines (based on git diff) only Jul 07, 2021 N/A N/A - :pypi:`pytest-nocustom` Run all tests without custom markers Jul 07, 2021 5 - Production/Stable N/A - :pypi:`pytest-nodev` Test-driven source code search for Python. Jul 21, 2016 4 - Beta pytest (>=2.8.1) - :pypi:`pytest-nogarbage` Ensure a test produces no garbage Aug 29, 2021 5 - Production/Stable pytest (>=4.6.0) - :pypi:`pytest-notebook` A pytest plugin for testing Jupyter Notebooks Sep 16, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-notice` Send pytest execution result email Nov 05, 2020 N/A N/A - :pypi:`pytest-notification` A pytest plugin for sending a desktop notification and playing a sound upon completion of tests Jun 19, 2020 N/A pytest (>=4) - :pypi:`pytest-notifier` A pytest plugin to notify test result Jun 12, 2020 3 - Alpha pytest - :pypi:`pytest-notimplemented` Pytest markers for not implemented features and tests. Aug 27, 2019 N/A pytest (>=5.1,<6.0) - :pypi:`pytest-notion` A PyTest Reporter to send test runs to Notion.so Aug 07, 2019 N/A N/A - :pypi:`pytest-nunit` A pytest plugin for generating NUnit3 test result XML output Aug 04, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-ochrus` pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A - :pypi:`pytest-odoo` py.test plugin to run Odoo tests Nov 04, 2021 4 - Beta pytest (>=2.9) - :pypi:`pytest-odoo-fixtures` Project description Jun 25, 2019 N/A N/A - :pypi:`pytest-oerp` pytest plugin to test OpenERP modules Feb 28, 2012 3 - Alpha N/A - :pypi:`pytest-ok` The ultimate pytest output plugin Apr 01, 2019 4 - Beta N/A - :pypi:`pytest-only` Use @pytest.mark.only to run a single test Jan 19, 2020 N/A N/A - :pypi:`pytest-oot` Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A - :pypi:`pytest-openfiles` Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) - :pypi:`pytest-opentmi` pytest plugin for publish results to opentmi Nov 04, 2021 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-operator` Fixtures for Operators Oct 26, 2021 N/A N/A - :pypi:`pytest-optional` include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A - :pypi:`pytest-optional-tests` Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) - :pypi:`pytest-orchestration` A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A - :pypi:`pytest-order` pytest plugin to run your tests in a specific order May 30, 2021 4 - Beta pytest (>=5.0) - :pypi:`pytest-ordering` pytest plugin to run your tests in a specific order Nov 14, 2018 4 - Beta pytest - :pypi:`pytest-osxnotify` OS X notifications for py.test results. May 15, 2015 N/A N/A - :pypi:`pytest-otel` pytest-otel report OpenTelemetry traces about test executed Dec 03, 2021 N/A N/A - :pypi:`pytest-pact` A simple plugin to use with pytest Jan 07, 2019 4 - Beta N/A - :pypi:`pytest-pahrametahrize` Parametrize your tests with a Boston accent. Nov 24, 2021 4 - Beta pytest (>=6.0,<7.0) - :pypi:`pytest-parallel` a pytest plugin for parallel and concurrent testing Oct 10, 2021 3 - Alpha pytest (>=3.0.0) - :pypi:`pytest-parallel-39` a pytest plugin for parallel and concurrent testing Jul 12, 2021 3 - Alpha pytest (>=3.0.0) - :pypi:`pytest-param` pytest plugin to test all, first, last or random params Sep 11, 2016 4 - Beta pytest (>=2.6.0) - :pypi:`pytest-paramark` Configure pytest fixtures using a combination of"parametrize" and markers Jan 10, 2020 4 - Beta pytest (>=4.5.0) - :pypi:`pytest-parametrization` Simpler PyTest parametrization Nov 30, 2021 5 - Production/Stable pytest - :pypi:`pytest-parametrize-cases` A more user-friendly way to write parametrized tests. Dec 12, 2020 N/A pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-parametrized` Pytest plugin for parametrizing tests with default iterables. Oct 19, 2020 5 - Production/Stable pytest - :pypi:`pytest-parawtf` Finally spell paramete?ri[sz]e correctly Dec 03, 2018 4 - Beta pytest (>=3.6.0) - :pypi:`pytest-pass` Check out https://github.com/elilutsky/pytest-pass Dec 04, 2019 N/A N/A - :pypi:`pytest-passrunner` Pytest plugin providing the 'run_on_pass' marker Feb 10, 2021 5 - Production/Stable pytest (>=4.6.0) - :pypi:`pytest-paste-config` Allow setting the path to a paste config file Sep 18, 2013 3 - Alpha N/A - :pypi:`pytest-patches` A contextmanager pytest fixture for handling multiple mock patches Aug 30, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-pdb` pytest plugin which adds pdb helper commands related to pytest. Jul 31, 2018 N/A N/A - :pypi:`pytest-peach` pytest plugin for fuzzing with Peach API Security Apr 12, 2019 4 - Beta pytest (>=2.8.7) - :pypi:`pytest-pep257` py.test plugin for pep257 Jul 09, 2016 N/A N/A - :pypi:`pytest-pep8` pytest plugin to check PEP8 requirements Apr 27, 2014 N/A N/A - :pypi:`pytest-percent` Change the exit code of pytest test sessions when a required percent of tests pass. May 21, 2020 N/A pytest (>=5.2.0) - :pypi:`pytest-perf` pytest-perf Jun 27, 2021 5 - Production/Stable pytest (>=4.6) ; extra == 'testing' - :pypi:`pytest-performance` A simple plugin to ensure the execution of critical sections of code has not been impacted Sep 11, 2020 5 - Production/Stable pytest (>=3.7.0) - :pypi:`pytest-persistence` Pytest tool for persistent objects Nov 06, 2021 N/A N/A - :pypi:`pytest-pgsql` Pytest plugins and helpers for tests using a Postgres database. May 13, 2020 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-phmdoctest` pytest plugin to test Python examples in Markdown using phmdoctest. Nov 10, 2021 4 - Beta pytest (>=6.2) ; extra == 'test' - :pypi:`pytest-picked` Run the tests related to the changed files Dec 23, 2020 N/A pytest (>=3.5.0) - :pypi:`pytest-pigeonhole` Jun 25, 2018 5 - Production/Stable pytest (>=3.4) - :pypi:`pytest-pikachu` Show surprise when tests are passing Aug 05, 2021 5 - Production/Stable pytest - :pypi:`pytest-pilot` Slice in your test base thanks to powerful markers. Oct 09, 2020 5 - Production/Stable N/A - :pypi:`pytest-pings` 🦊 The pytest plugin for Firefox Telemetry 📊 Jun 29, 2019 3 - Alpha pytest (>=5.0.0) - :pypi:`pytest-pinned` A simple pytest plugin for pinning tests Sep 17, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-pinpoint` A pytest plugin which runs SBFL algorithms to detect faults. Sep 25, 2020 N/A pytest (>=4.4.0) - :pypi:`pytest-pipeline` Pytest plugin for functional testing of data analysispipelines Jan 24, 2017 3 - Alpha N/A - :pypi:`pytest-platform-markers` Markers for pytest to skip tests on specific platforms Sep 09, 2019 4 - Beta pytest (>=3.6.0) - :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A - :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Oct 28, 2021 N/A pytest - :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A - :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A - :pypi:`pytest-plt` Fixtures for quickly making Matplotlib plots in tests Aug 17, 2020 5 - Production/Stable pytest - :pypi:`pytest-plugin-helpers` A plugin to help developing and testing other plugins Nov 23, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-plus` PyTest Plus Plugin :: extends pytest functionality Mar 19, 2020 5 - Production/Stable pytest (>=3.50) - :pypi:`pytest-pmisc` Mar 21, 2019 5 - Production/Stable N/A - :pypi:`pytest-pointers` Pytest plugin to define functions you test with special marks for better navigation and reports Oct 14, 2021 N/A N/A - :pypi:`pytest-polarion-cfme` pytest plugin for collecting test cases and recording test results Nov 13, 2017 3 - Alpha N/A - :pypi:`pytest-polarion-collect` pytest plugin for collecting polarion test cases data Jun 18, 2020 3 - Alpha pytest - :pypi:`pytest-polecat` Provides Polecat pytest fixtures Aug 12, 2019 4 - Beta N/A - :pypi:`pytest-ponyorm` PonyORM in Pytest Oct 31, 2018 N/A pytest (>=3.1.1) - :pypi:`pytest-poo` Visualize your crappy tests Mar 25, 2021 5 - Production/Stable pytest (>=2.3.4) - :pypi:`pytest-poo-fail` Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A - :pypi:`pytest-pop` A pytest plugin to help with testing pop projects Aug 19, 2021 5 - Production/Stable pytest - :pypi:`pytest-portion` Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-postgres` Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest - :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. Nov 05, 2021 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-power` pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) - :pypi:`pytest-pretty-terminal` pytest plugin for generating prettier terminal output Nov 24, 2021 N/A pytest (>=3.4.1) - :pypi:`pytest-pride` Minitest-style test colors Apr 02, 2016 3 - Alpha N/A - :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Jun 17, 2021 5 - Production/Stable pytest (>=6) - :pypi:`pytest-profiling` Profiling plugin for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-progress` pytest plugin for instant test progress status Nov 09, 2021 5 - Production/Stable pytest (>=2.7) - :pypi:`pytest-prometheus` Report test pass / failures to a Prometheus PushGateway Oct 03, 2017 N/A N/A - :pypi:`pytest-prosper` Test helpers for Prosper projects Sep 24, 2018 N/A N/A - :pypi:`pytest-pspec` A rspec format reporter for Python ptest Jun 02, 2020 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-psqlgraph` pytest plugin for testing applications that use psqlgraph Oct 19, 2021 4 - Beta pytest (>=6.0) - :pypi:`pytest-ptera` Use ptera probes in tests Oct 20, 2021 N/A pytest (>=6.2.4,<7.0.0) - :pypi:`pytest-pudb` Pytest PuDB debugger integration Oct 25, 2018 3 - Alpha pytest (>=2.0) - :pypi:`pytest-purkinje` py.test plugin for purkinje test runner Oct 28, 2017 2 - Pre-Alpha N/A - :pypi:`pytest-pycharm` Plugin for py.test to enter PyCharm debugger on uncaught exceptions Aug 13, 2020 5 - Production/Stable pytest (>=2.3) - :pypi:`pytest-pycodestyle` pytest plugin to run pycodestyle Aug 10, 2020 3 - Alpha N/A - :pypi:`pytest-pydev` py.test plugin to connect to a remote debug server with PyDev or PyCharm. Nov 15, 2017 3 - Alpha N/A - :pypi:`pytest-pydocstyle` pytest plugin to run pydocstyle Aug 10, 2020 3 - Alpha N/A - :pypi:`pytest-pylint` pytest plugin to check source code with pylint Nov 09, 2020 5 - Production/Stable pytest (>=5.4) - :pypi:`pytest-pypi` Easily test your HTTP library against a local copy of pypi Mar 04, 2018 3 - Alpha N/A - :pypi:`pytest-pypom-navigation` Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) - :pypi:`pytest-pyppeteer` A plugin to run pyppeteer in pytest. Feb 16, 2021 4 - Beta pytest (>=6.0.2) - :pypi:`pytest-pyq` Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A - :pypi:`pytest-pyramid` pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite Oct 15, 2021 5 - Production/Stable pytest - :pypi:`pytest-pyramid-server` Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-pyright` Pytest plugin for type checking code with Pyright Aug 16, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-pytestrail` Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) - :pypi:`pytest-pythonpath` pytest plugin for adding to the PYTHONPATH from command line or configs. Aug 22, 2018 5 - Production/Stable N/A - :pypi:`pytest-pytorch` pytest plugin for a better developer experience when working with the PyTorch test suite May 25, 2021 4 - Beta pytest - :pypi:`pytest-qasync` Pytest support for qasync. Jul 12, 2021 4 - Beta pytest (>=5.4.0) - :pypi:`pytest-qatouch` Pytest plugin for uploading test results to your QA Touch Testrun. Jun 26, 2021 4 - Beta pytest (>=6.2.0) - :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Nov 25, 2021 5 - Production/Stable pytest (>=6.2.3) - :pypi:`pytest-qml` Run QML Tests with pytest Dec 02, 2020 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-qr` pytest plugin to generate test result QR codes Nov 25, 2021 4 - Beta N/A - :pypi:`pytest-qt` pytest support for PyQt and PySide applications Jun 13, 2021 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-qt-app` QT app fixture for py.test Dec 23, 2015 5 - Production/Stable N/A - :pypi:`pytest-quarantine` A plugin for pytest to manage expected test failures Nov 24, 2019 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-quickcheck` pytest plugin to generate random data inspired by QuickCheck Nov 15, 2020 4 - Beta pytest (<6.0.0,>=4.0) - :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jun 02, 2021 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-race` Race conditions tester for pytest Nov 21, 2016 4 - Beta N/A - :pypi:`pytest-rage` pytest plugin to implement PEP712 Oct 21, 2011 3 - Alpha N/A - :pypi:`pytest-railflow-testrail-reporter` Generate json reports along with specified metadata defined in test markers. Dec 02, 2021 5 - Production/Stable pytest - :pypi:`pytest-raises` An implementation of pytest.raises as a pytest.mark fixture Apr 23, 2020 N/A pytest (>=3.2.2) - :pypi:`pytest-raisesregexp` Simple pytest plugin to look for regex in Exceptions Dec 18, 2015 N/A N/A - :pypi:`pytest-raisin` Plugin enabling the use of exception instances with pytest.raises Jun 25, 2020 N/A pytest - :pypi:`pytest-random` py.test plugin to randomize tests Apr 28, 2013 3 - Alpha N/A - :pypi:`pytest-randomly` Pytest plugin to randomly order tests and control random.seed. Nov 30, 2021 5 - Production/Stable pytest - :pypi:`pytest-randomness` Pytest plugin about random seed management May 30, 2019 3 - Alpha N/A - :pypi:`pytest-random-num` Randomise the order in which pytest tests are run with some control over the randomness Oct 19, 2020 5 - Production/Stable N/A - :pypi:`pytest-random-order` Randomise the order in which pytest tests are run with some control over the randomness Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-readme` Test your README.md file Dec 28, 2014 5 - Production/Stable N/A - :pypi:`pytest-reana` Pytest fixtures for REANA. Nov 22, 2021 3 - Alpha N/A - :pypi:`pytest-recording` A pytest plugin that allows you recording of network interactions via VCR.py Jul 08, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-recordings` Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal Aug 13, 2020 N/A N/A - :pypi:`pytest-redis` Redis fixtures and fixture factories for Pytest. Nov 03, 2021 5 - Production/Stable pytest - :pypi:`pytest-redislite` Pytest plugin for testing code using Redis Sep 19, 2021 4 - Beta pytest - :pypi:`pytest-redmine` Pytest plugin for redmine Mar 19, 2018 1 - Planning N/A - :pypi:`pytest-ref` A plugin to store reference files to ease regression testing Nov 23, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-reference-formatter` Conveniently run pytest with a dot-formatted test reference. Oct 01, 2019 4 - Beta N/A - :pypi:`pytest-regressions` Easy to use fixtures to write regression tests. Jan 27, 2021 5 - Production/Stable pytest (>=3.5.0) - :pypi:`pytest-regtest` pytest plugin for regression tests Jun 03, 2021 N/A N/A - :pypi:`pytest-relative-order` a pytest plugin that sorts tests using "before" and "after" markers May 17, 2021 4 - Beta N/A - :pypi:`pytest-relaxed` Relaxed test discovery/organization for pytest Jun 14, 2019 5 - Production/Stable pytest (<5,>=3) - :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A - :pypi:`pytest-remotedata` Pytest plugin for controlling remote data access. Jul 20, 2019 3 - Alpha pytest (>=3.1) - :pypi:`pytest-remote-response` Pytest plugin for capturing and mocking connection requests. Jun 30, 2021 4 - Beta pytest (>=4.6) - :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Mar 04, 2020 4 - Beta pytest - :pypi:`pytest-reorder` Reorder tests depending on their paths and names. May 31, 2018 4 - Beta pytest - :pypi:`pytest-repeat` pytest plugin for repeating tests Oct 31, 2020 5 - Production/Stable pytest (>=3.6) - :pypi:`pytest-replay` Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Jun 09, 2021 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-repo-health` A pytest plugin to report on repository standards conformance Nov 23, 2021 3 - Alpha pytest - :pypi:`pytest-report` Creates json report that is compatible with atom.io's linter message format May 11, 2016 4 - Beta N/A - :pypi:`pytest-reporter` Generate Pytest reports with templates Jul 22, 2021 4 - Beta pytest - :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Jun 08, 2021 4 - Beta N/A - :pypi:`pytest-reportinfra` Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A - :pypi:`pytest-reporting` A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility Dec 11, 2020 3 - Alpha pytest (>=5.2) - :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest - :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Jun 18, 2021 N/A pytest (>=3.8.0) - :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) - :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-reraise` Make multi-threaded pytest test cases fail when they should Jun 17, 2021 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-rerun` Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) - :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Sep 17, 2021 5 - Production/Stable pytest (>=5.3) - :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest. Nov 15, 2021 N/A N/A - :pypi:`pytest-resource` Load resource fixture plugin to use with pytest Nov 14, 2018 4 - Beta N/A - :pypi:`pytest-resource-path` Provides path for uniform access to test resources in isolated directory May 01, 2021 5 - Production/Stable pytest (>=3.5.0) - :pypi:`pytest-responsemock` Simplified requests calls mocking for pytest Oct 10, 2020 5 - Production/Stable N/A - :pypi:`pytest-responses` py.test integration for responses Apr 26, 2021 N/A pytest (>=2.5) - :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed Aug 12, 2021 5 - Production/Stable pytest - :pypi:`pytest-rethinkdb` A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A - :pypi:`pytest-reverse` Pytest plugin to reverse test order. Aug 12, 2021 5 - Production/Stable pytest - :pypi:`pytest-ringo` pytest plugin to test webapplications using the Ringo webframework Sep 27, 2017 3 - Alpha N/A - :pypi:`pytest-rng` Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest - :pypi:`pytest-roast` pytest plugin for ROAST configuration override and fixtures Jul 29, 2021 5 - Production/Stable pytest - :pypi:`pytest-rocketchat` Pytest to Rocket.Chat reporting plugin Apr 18, 2021 5 - Production/Stable N/A - :pypi:`pytest-rotest` Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) - :pypi:`pytest-rpc` Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) - :pypi:`pytest-rst` Test code from RST documents with pytest Sep 21, 2021 N/A pytest - :pypi:`pytest-rt` pytest data collector plugin for Testgr Sep 04, 2021 N/A N/A - :pypi:`pytest-rts` Coverage-based regression test selection (RTS) plugin for pytest May 17, 2021 N/A pytest - :pypi:`pytest-run-changed` Pytest plugin that runs changed tests only Apr 02, 2021 3 - Alpha pytest - :pypi:`pytest-runfailed` implement a --failed option for pytest Mar 24, 2016 N/A N/A - :pypi:`pytest-runner` Invoke py.test as distutils command with dependency resolution May 19, 2021 5 - Production/Stable pytest (>=4.6) ; extra == 'testing' - :pypi:`pytest-runtime-xfail` Call runtime_xfail() to mark running test as xfail. Aug 26, 2021 N/A N/A - :pypi:`pytest-salt` Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A - :pypi:`pytest-salt-containers` A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A - :pypi:`pytest-salt-factories` Pytest Salt Plugin Sep 16, 2021 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-salt-from-filenames` Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) - :pypi:`pytest-salt-runtests-bridge` Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) - :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) - :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A - :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Dec 03, 2021 5 - Production/Stable N/A - :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A - :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Aug 31, 2020 5 - Production/Stable pytest (>=3.5.0) - :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A - :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) - :pypi:`pytest-selenium` pytest plugin for Selenium Sep 19, 2020 5 - Production/Stable pytest (>=5.0.0) - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Dec 03, 2021 5 - Production/Stable N/A - :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Nov 26, 2020 5 - Production/Stable N/A - :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A - :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A - :pypi:`pytest-sentry` A pytest plugin to send testrun information to Sentry.io Apr 21, 2021 N/A pytest - :pypi:`pytest-server-fixtures` Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-serverless` Automatically mocks resources from serverless.yml in pytest using moto. Nov 27, 2021 4 - Beta N/A - :pypi:`pytest-services` Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A - :pypi:`pytest-session2file` pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest - :pypi:`pytest-session-fixture-globalize` py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A - :pypi:`pytest-session_to_file` pytest-session_to_file is a py.test plugin for capturing and saving to file the stdout of py.test. Oct 01, 2015 3 - Alpha N/A - :pypi:`pytest-sftpserver` py.test plugin to locally test sftp server connections. Sep 16, 2019 4 - Beta N/A - :pypi:`pytest-shard` Dec 11, 2020 4 - Beta pytest - :pypi:`pytest-shell` A pytest plugin to help with testing shell scripts / black box commands Nov 07, 2021 N/A N/A - :pypi:`pytest-sheraf` Versatile ZODB abstraction layer - pytest fixtures Feb 11, 2020 N/A pytest - :pypi:`pytest-sherlock` pytest plugin help to find coupled tests Nov 18, 2021 5 - Production/Stable pytest (>=3.5.1) - :pypi:`pytest-shortcuts` Expand command-line shortcuts listed in pytest configuration Oct 29, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-shutil` A goodie-bag of unix shell and environment tools for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-simplehttpserver` Simple pytest fixture to spin up an HTTP server Jun 24, 2021 4 - Beta N/A - :pypi:`pytest-simple-plugin` Simple pytest plugin Nov 27, 2019 N/A N/A - :pypi:`pytest-simple-settings` simple-settings plugin for pytest Nov 17, 2020 4 - Beta pytest - :pypi:`pytest-single-file-logging` Allow for multiple processes to log to a single file May 05, 2016 4 - Beta pytest (>=2.8.1) - :pypi:`pytest-skip-markers` Pytest Salt Plugin Oct 04, 2021 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-skipper` A plugin that selects only tests with changes in execution path Mar 26, 2017 3 - Alpha pytest (>=3.0.6) - :pypi:`pytest-skippy` Automatically skip tests that don't need to run! Jan 27, 2018 3 - Alpha pytest (>=2.3.4) - :pypi:`pytest-skip-slow` A pytest plugin to skip \`@pytest.mark.slow\` tests by default. Sep 28, 2021 N/A N/A - :pypi:`pytest-slack` Pytest to Slack reporting plugin Dec 15, 2020 5 - Production/Stable N/A - :pypi:`pytest-slow` A pytest plugin to skip \`@pytest.mark.slow\` tests by default. Sep 28, 2021 N/A N/A - :pypi:`pytest-smartcollect` A plugin for collecting tests that touch changed code Oct 04, 2018 N/A pytest (>=3.5.0) - :pypi:`pytest-smartcov` Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A - :pypi:`pytest-smtp` Send email with pytest execution result Feb 20, 2021 N/A pytest - :pypi:`pytest-snail` Plugin for adding a marker to slow running tests. 🐌 Nov 04, 2019 3 - Alpha pytest (>=5.0.1) - :pypi:`pytest-snapci` py.test plugin for Snap-CI Nov 12, 2015 N/A N/A - :pypi:`pytest-snapshot` A plugin for snapshot testing with pytest. Dec 02, 2021 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-snmpserver` May 12, 2021 N/A N/A - :pypi:`pytest-socket` Pytest Plugin to disable socket calls during tests Aug 28, 2021 4 - Beta pytest (>=3.6.3) - :pypi:`pytest-soft-assertions` May 05, 2020 3 - Alpha pytest - :pypi:`pytest-solr` Solr process and client fixtures for py.test. May 11, 2020 3 - Alpha pytest (>=3.0.0) - :pypi:`pytest-sorter` A simple plugin to first execute tests that historically failed more Apr 20, 2021 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-sourceorder` Test-ordering plugin for pytest Sep 01, 2021 4 - Beta pytest - :pypi:`pytest-spark` pytest plugin to run the tests with support of pyspark. Feb 23, 2020 4 - Beta pytest - :pypi:`pytest-spawner` py.test plugin to spawn process and communicate with them. Jul 31, 2015 4 - Beta N/A - :pypi:`pytest-spec` Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION. May 04, 2021 N/A N/A - :pypi:`pytest-sphinx` Doctest plugin for pytest with support for Sphinx-specific doctest-directives Aug 05, 2020 4 - Beta N/A - :pypi:`pytest-spiratest` Exports unit tests as test runs in SpiraTest/Team/Plan Oct 13, 2021 N/A N/A - :pypi:`pytest-splinter` Splinter plugin for pytest testing framework Dec 25, 2020 6 - Mature N/A - :pypi:`pytest-split` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Nov 09, 2021 4 - Beta pytest (>=5,<7) - :pypi:`pytest-splitio` Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) - :pypi:`pytest-split-tests` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. Jul 30, 2021 5 - Production/Stable pytest (>=2.5) - :pypi:`pytest-split-tests-tresorit` Feb 22, 2021 1 - Planning N/A - :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Nov 29, 2021 N/A pytest (>5.4.0,<6.3) - :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Oct 07, 2021 N/A N/A - :pypi:`pytest-splunk-env` pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) - :pypi:`pytest-sqitch` sqitch for pytest Apr 06, 2020 4 - Beta N/A - :pypi:`pytest-sqlalchemy` pytest plugin with sqlalchemy related fixtures Mar 13, 2018 3 - Alpha N/A - :pypi:`pytest-sql-bigquery` Yet another SQL-testing framework for BigQuery provided by pytest plugin Dec 19, 2019 N/A pytest - :pypi:`pytest-srcpaths` Add paths to sys.path Oct 15, 2021 N/A N/A - :pypi:`pytest-ssh` pytest plugin for ssh command run May 27, 2019 N/A pytest - :pypi:`pytest-start-from` Start pytest run from a given point Apr 11, 2016 N/A N/A - :pypi:`pytest-statsd` pytest plugin for reporting to graphite Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) - :pypi:`pytest-stepfunctions` A small description May 08, 2021 4 - Beta pytest - :pypi:`pytest-steps` Create step-wise / incremental tests in pytest. Sep 23, 2021 5 - Production/Stable N/A - :pypi:`pytest-stepwise` Run a test suite one failing test at a time. Dec 01, 2015 4 - Beta N/A - :pypi:`pytest-stoq` A plugin to pytest stoq Feb 09, 2021 4 - Beta N/A - :pypi:`pytest-stress` A Pytest plugin that allows you to loop tests for a user defined amount of time. Dec 07, 2019 4 - Beta pytest (>=3.6.0) - :pypi:`pytest-structlog` Structured logging assertions Sep 21, 2021 N/A pytest - :pypi:`pytest-structmpd` provide structured temporary directory Oct 17, 2018 N/A N/A - :pypi:`pytest-stub` Stub packages, modules and attributes. Apr 28, 2020 5 - Production/Stable N/A - :pypi:`pytest-stubprocess` Provide stub implementations for subprocesses in Python tests Sep 17, 2018 3 - Alpha pytest (>=3.5.0) - :pypi:`pytest-study` A pytest plugin to organize long run tests (named studies) without interfering the regular tests Sep 26, 2017 3 - Alpha pytest (>=2.0) - :pypi:`pytest-subprocess` A plugin to fake subprocess for pytest Nov 07, 2021 5 - Production/Stable pytest (>=4.0.0) - :pypi:`pytest-subtesthack` A hack to explicitly set up and tear down fixtures. Mar 02, 2021 N/A N/A - :pypi:`pytest-subtests` unittest subTest() support and subtests fixture May 29, 2021 4 - Beta pytest (>=5.3.0) - :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Aug 29, 2017 N/A N/A - :pypi:`pytest-sugar` pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Jul 06, 2020 3 - Alpha N/A - :pypi:`pytest-sugar-bugfix159` Workaround for https://github.com/Frozenball/pytest-sugar/issues/159 Nov 07, 2018 5 - Production/Stable pytest (!=3.7.3,>=3.5); extra == 'testing' - :pypi:`pytest-super-check` Pytest plugin to check your TestCase classes call super in setUp, tearDown, etc. Aug 12, 2021 5 - Production/Stable pytest - :pypi:`pytest-svn` SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-symbols` pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A - :pypi:`pytest-takeltest` Fixtures for ansible, testinfra and molecule Oct 13, 2021 N/A N/A - :pypi:`pytest-talisker` Nov 28, 2021 N/A N/A - :pypi:`pytest-tap` Test Anything Protocol (TAP) reporting plugin for pytest Oct 27, 2021 5 - Production/Stable pytest (>=3.0) - :pypi:`pytest-tape` easy assertion with expected results saved to yaml files Mar 17, 2021 4 - Beta N/A - :pypi:`pytest-target` Pytest plugin for remote target orchestration. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) - :pypi:`pytest-tblineinfo` tblineinfo is a py.test plugin that insert the node id in the final py.test report when --tb=line option is used Dec 01, 2015 3 - Alpha pytest (>=2.0) - :pypi:`pytest-teamcity-logblock` py.test plugin to introduce block structure in teamcity build log, if output is not captured May 15, 2018 4 - Beta N/A - :pypi:`pytest-telegram` Pytest to Telegram reporting plugin Dec 10, 2020 5 - Production/Stable N/A - :pypi:`pytest-tempdir` Predictable and repeatable tempdir support. Oct 11, 2019 4 - Beta pytest (>=2.8.1) - :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures Nov 10, 2021 N/A pytest (>=6.0) - :pypi:`pytest-terraform-fixture` generate terraform resources to use with pytest Nov 14, 2018 4 - Beta N/A - :pypi:`pytest-testbook` A plugin to run tests written in Jupyter notebook Dec 11, 2016 3 - Alpha N/A - :pypi:`pytest-testconfig` Test configuration plugin for pytest. Jan 11, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-testdirectory` A py.test plugin providing temporary directories in unit tests. Nov 06, 2018 5 - Production/Stable pytest - :pypi:`pytest-testdox` A testdox format reporter for pytest Oct 13, 2020 5 - Production/Stable pytest (>=3.7.0) - :pypi:`pytest-test-groups` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Oct 25, 2016 5 - Production/Stable N/A - :pypi:`pytest-testinfra` Test infrastructures Jun 20, 2021 5 - Production/Stable pytest (!=3.0.2) - :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) - :pypi:`pytest-testmon` selects tests affected by changed files and methods Oct 22, 2021 4 - Beta N/A - :pypi:`pytest-testobject` Plugin to use TestObject Suites with Pytest Sep 24, 2019 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-testrail` pytest plugin for creating TestRail runs and adding results Aug 27, 2020 N/A pytest (>=3.6) - :pypi:`pytest-testrail2` A small example package Nov 17, 2020 N/A pytest (>=5) - :pypi:`pytest-testrail-api` Плагин Pytest, для интеграции с TestRail Nov 30, 2021 N/A pytest (>=5.5) - :pypi:`pytest-testrail-api-client` TestRail Api Python Client Dec 03, 2021 N/A pytest - :pypi:`pytest-testrail-appetize` pytest plugin for creating TestRail runs and adding results Sep 29, 2021 N/A N/A - :pypi:`pytest-testrail-client` pytest plugin for Testrail Sep 29, 2020 5 - Production/Stable N/A - :pypi:`pytest-testrail-e2e` pytest plugin for creating TestRail runs and adding results Oct 11, 2021 N/A pytest (>=3.6) - :pypi:`pytest-testrail-ns` pytest plugin for creating TestRail runs and adding results Oct 08, 2021 N/A pytest (>=3.6) - :pypi:`pytest-testrail-plugin` PyTest plugin for TestRail Apr 21, 2020 3 - Alpha pytest - :pypi:`pytest-testrail-reporter` Sep 10, 2018 N/A N/A - :pypi:`pytest-testreport` Nov 12, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-testslide` TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) - :pypi:`pytest-test-this` Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply Sep 15, 2019 2 - Pre-Alpha pytest (>=2.3) - :pypi:`pytest-test-utils` Nov 30, 2021 N/A pytest (>=5) - :pypi:`pytest-tesults` Tesults plugin for pytest Jul 31, 2021 5 - Production/Stable pytest (>=3.5.0) - :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A - :pypi:`pytest-thawgun` Pytest plugin for time travel May 26, 2020 3 - Alpha N/A - :pypi:`pytest-threadleak` Detects thread leaks Sep 08, 2017 4 - Beta N/A - :pypi:`pytest-tick` Ticking on tests Aug 31, 2021 5 - Production/Stable pytest (>=6.2.5,<7.0.0) - :pypi:`pytest-timeit` A pytest plugin to time test function runs Oct 13, 2016 4 - Beta N/A - :pypi:`pytest-timeout` pytest plugin to abort hanging tests Oct 11, 2021 5 - Production/Stable pytest (>=5.0.0) - :pypi:`pytest-timeouts` Linux-only Pytest plugin to control durations of various test case execution phases Sep 21, 2019 5 - Production/Stable N/A - :pypi:`pytest-timer` A timer plugin for pytest Jun 02, 2021 N/A N/A - :pypi:`pytest-timestamper` Pytest plugin to add a timestamp prefix to the pytest output Jun 06, 2021 N/A N/A - :pypi:`pytest-tipsi-django` Nov 17, 2021 4 - Beta pytest (>=6.0.0) - :pypi:`pytest-tipsi-testing` Better fixtures management. Various helpers Nov 04, 2020 4 - Beta pytest (>=3.3.0) - :pypi:`pytest-tldr` A pytest plugin that limits the output to just the things you need. Mar 12, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-tm4j-reporter` Cloud Jira Test Management (TM4J) PyTest reporter plugin Sep 01, 2020 N/A pytest - :pypi:`pytest-tmreport` this is a vue-element ui report for pytest Nov 17, 2021 N/A N/A - :pypi:`pytest-todo` A small plugin for the pytest testing framework, marking TODO comments as failure May 23, 2019 4 - Beta pytest - :pypi:`pytest-tomato` Mar 01, 2019 5 - Production/Stable N/A - :pypi:`pytest-toolbelt` This is just a collection of utilities for pytest, but don't really belong in pytest proper. Aug 12, 2019 3 - Alpha N/A - :pypi:`pytest-toolbox` Numerous useful plugins for pytest. Apr 07, 2018 N/A pytest (>=3.5.0) - :pypi:`pytest-tornado` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Jun 17, 2020 5 - Production/Stable pytest (>=3.6) - :pypi:`pytest-tornado5` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Nov 16, 2018 5 - Production/Stable pytest (>=3.6) - :pypi:`pytest-tornado-yen3` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Oct 15, 2018 5 - Production/Stable N/A - :pypi:`pytest-tornasync` py.test plugin for testing Python 3.5+ Tornado code Jul 15, 2019 3 - Alpha pytest (>=3.0) - :pypi:`pytest-track` Feb 26, 2021 3 - Alpha pytest (>=3.0) - :pypi:`pytest-translations` Test your translation files. Nov 05, 2021 5 - Production/Stable N/A - :pypi:`pytest-travis-fold` Folds captured output sections in Travis CI build log Nov 29, 2017 4 - Beta pytest (>=2.6.0) - :pypi:`pytest-trello` Plugin for py.test that integrates trello using markers Nov 20, 2015 5 - Production/Stable N/A - :pypi:`pytest-trepan` Pytest plugin for trepan debugger. Jul 28, 2018 5 - Production/Stable N/A - :pypi:`pytest-trialtemp` py.test plugin for using the same _trial_temp working directory as trial Jun 08, 2015 N/A N/A - :pypi:`pytest-trio` Pytest plugin for trio Oct 16, 2020 N/A N/A - :pypi:`pytest-tspwplib` A simple plugin to use with tspwplib Jan 08, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-tstcls` Test Class Base Mar 23, 2020 5 - Production/Stable N/A - :pypi:`pytest-twisted` A twisted plugin for pytest. Aug 30, 2021 5 - Production/Stable pytest (>=2.3) - :pypi:`pytest-typhoon-xray` Typhoon HIL plugin for pytest Nov 03, 2021 4 - Beta N/A - :pypi:`pytest-tytest` Typhoon HIL plugin for pytest May 25, 2020 4 - Beta pytest (>=5.4.2) - :pypi:`pytest-ubersmith` Easily mock calls to ubersmith at the \`requests\` level. Apr 13, 2015 N/A N/A - :pypi:`pytest-ui` Text User Interface for running python tests Jul 05, 2021 4 - Beta pytest - :pypi:`pytest-unhandled-exception-exit-code` Plugin for py.test set a different exit code on uncaught exceptions Jun 22, 2020 5 - Production/Stable pytest (>=2.3) - :pypi:`pytest-unittest-filter` A pytest plugin for filtering unittest-based test classes Jan 12, 2019 4 - Beta pytest (>=3.1.0) - :pypi:`pytest-unmarked` Run only unmarked tests Aug 27, 2019 5 - Production/Stable N/A - :pypi:`pytest-unordered` Test equality of unordered collections in pytest Mar 28, 2021 4 - Beta N/A - :pypi:`pytest-upload-report` pytest-upload-report is a plugin for pytest that upload your test report for test results. Jun 18, 2021 5 - Production/Stable N/A - :pypi:`pytest-utils` Some helpers for pytest. Dec 04, 2021 4 - Beta pytest (>=6.2.5,<7.0.0) - :pypi:`pytest-vagrant` A py.test plugin providing access to vagrant. Sep 07, 2021 5 - Production/Stable pytest - :pypi:`pytest-valgrind` May 19, 2021 N/A N/A - :pypi:`pytest-variables` pytest plugin for providing variables to tests/fixtures Oct 23, 2019 5 - Production/Stable pytest (>=2.4.2) - :pypi:`pytest-variant` Variant support for Pytest Jun 20, 2021 N/A N/A - :pypi:`pytest-vcr` Plugin for managing VCR.py cassettes Apr 26, 2019 5 - Production/Stable pytest (>=3.6.0) - :pypi:`pytest-vcr-delete-on-fail` A pytest plugin that automates vcrpy cassettes deletion on test failure. Aug 13, 2021 4 - Beta pytest (>=6.2.2,<7.0.0) - :pypi:`pytest-vcrpandas` Test from HTTP interactions to dataframe processed. Jan 12, 2019 4 - Beta pytest - :pypi:`pytest-venv` py.test fixture for creating a virtual environment Aug 04, 2020 4 - Beta pytest - :pypi:`pytest-ver` Pytest module with Verification Report Aug 30, 2021 2 - Pre-Alpha N/A - :pypi:`pytest-verbose-parametrize` More descriptive output for parametrized py.test tests May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-vimqf` A simple pytest plugin that will shrink pytest output when specified, to fit vim quickfix window. Feb 08, 2021 4 - Beta pytest (>=6.2.2,<7.0.0) - :pypi:`pytest-virtualenv` Virtualenv fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-voluptuous` Pytest plugin for asserting data against voluptuous schema. Jun 09, 2020 N/A pytest - :pypi:`pytest-vscodedebug` A pytest plugin to easily enable debugging tests within Visual Studio Code Dec 04, 2020 4 - Beta N/A - :pypi:`pytest-vts` pytest plugin for automatic recording of http stubbed tests Jun 05, 2019 N/A pytest (>=2.3) - :pypi:`pytest-vw` pytest-vw makes your failing test cases succeed under CI tools scrutiny Oct 07, 2015 4 - Beta N/A - :pypi:`pytest-vyper` Plugin for the vyper smart contract language. May 28, 2020 2 - Pre-Alpha N/A - :pypi:`pytest-wa-e2e-plugin` Pytest plugin for testing whatsapp bots with end to end tests Feb 18, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-watch` Local continuous test runner with pytest and watchdog. May 20, 2018 N/A N/A - :pypi:`pytest-watcher` Continiously runs pytest on changes in \*.py files Sep 18, 2021 3 - Alpha N/A - :pypi:`pytest-wdl` Pytest plugin for testing WDL workflows. Nov 17, 2020 5 - Production/Stable N/A - :pypi:`pytest-webdriver` Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-wetest` Welian API Automation test framework pytest plugin Nov 10, 2018 4 - Beta N/A - :pypi:`pytest-whirlwind` Testing Tornado. Jun 12, 2020 N/A N/A - :pypi:`pytest-wholenodeid` pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) - :pypi:`pytest-win32consoletitle` Pytest progress in console title (Win32 only) Aug 08, 2021 N/A N/A - :pypi:`pytest-winnotify` Windows tray notifications for py.test results. Apr 22, 2016 N/A N/A - :pypi:`pytest-with-docker` pytest with docker helpers. Nov 09, 2021 N/A pytest - :pypi:`pytest-workflow` A pytest plugin for configuring workflow/pipeline tests using YAML files Dec 03, 2021 5 - Production/Stable pytest (>=5.4.0) - :pypi:`pytest-xdist` pytest xdist plugin for distributed testing and loop-on-failing modes Sep 21, 2021 5 - Production/Stable pytest (>=6.0.0) - :pypi:`pytest-xdist-debug-for-graingert` pytest xdist plugin for distributed testing and loop-on-failing modes Jul 24, 2019 5 - Production/Stable pytest (>=4.4.0) - :pypi:`pytest-xdist-forked` forked from pytest-xdist Feb 10, 2020 5 - Production/Stable pytest (>=4.4.0) - :pypi:`pytest-xdist-tracker` pytest plugin helps to reproduce failures for particular xdist node Nov 18, 2021 3 - Alpha pytest (>=3.5.1) - :pypi:`pytest-xfaillist` Maintain a xfaillist in an additional file to avoid merge-conflicts. Sep 17, 2021 N/A pytest (>=6.2.2,<7.0.0) - :pypi:`pytest-xfiles` Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A - :pypi:`pytest-xlog` Extended logging for test and decorators May 31, 2020 4 - Beta N/A - :pypi:`pytest-xpara` An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest - :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. Jul 28, 2021 4 - Beta pytest (>=2.8) - :pypi:`pytest-xray` May 30, 2019 3 - Alpha N/A - :pypi:`pytest-xrayjira` Mar 17, 2020 3 - Alpha pytest (==4.3.1) - :pypi:`pytest-xray-server` Oct 27, 2021 3 - Alpha pytest (>=5.3.1) - :pypi:`pytest-xvfb` A pytest plugin to run Xvfb for tests. Jun 09, 2020 4 - Beta pytest (>=2.8.1) - :pypi:`pytest-yaml` This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest - :pypi:`pytest-yamltree` Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-yamlwsgi` Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A - :pypi:`pytest-yapf` Run yapf Jul 06, 2017 4 - Beta pytest (>=3.1.1) - :pypi:`pytest-yapf3` Validate your Python file format with yapf Aug 03, 2020 5 - Production/Stable pytest (>=5.4) - :pypi:`pytest-yield` PyTest plugin to run tests concurrently, each \`yield\` switch context to other one Jan 23, 2019 N/A N/A - :pypi:`pytest-yuk` Display tests you are uneasy with, using 🤢/🤮 for pass/fail of tests marked with yuk. Mar 26, 2021 N/A N/A - :pypi:`pytest-zafira` A Zafira plugin for pytest Sep 18, 2019 5 - Production/Stable pytest (==4.1.1) - :pypi:`pytest-zap` OWASP ZAP plugin for py.test. May 12, 2014 4 - Beta N/A - :pypi:`pytest-zebrunner` Pytest connector for Zebrunner reporting Dec 02, 2021 5 - Production/Stable pytest (>=4.5.0) - :pypi:`pytest-zigzag` Extend py.test for RPC OpenStack testing. Feb 27, 2019 4 - Beta pytest (~=3.6) - =============================================== ======================================================================================================================================================================== ============== ===================== ================================================ + =============================================== ====================================================================================================================================================================================================================================================================================================================================================================================== ============== ===================== ================================================ + name summary last_release status requires + =============================================== ====================================================================================================================================================================================================================================================================================================================================================================================== ============== ===================== ================================================ + :pypi:`logassert` Simple but powerful assertion and verification of logged lines. May 20, 2022 5 - Production/Stable N/A + :pypi:`logot` Test whether your code is logging correctly 🪵 Mar 23, 2024 5 - Production/Stable pytest<9,>=7; extra == "pytest" + :pypi:`nuts` Network Unit Testing System Aug 11, 2023 N/A pytest (>=7.3.0,<8.0.0) + :pypi:`pytest-abq` Pytest integration for the ABQ universal test runner. Apr 07, 2023 N/A N/A + :pypi:`pytest-abstracts` A contextmanager pytest fixture for handling multiple mock abstracts May 25, 2022 N/A N/A + :pypi:`pytest-accept` A pytest-plugin for updating doctest outputs Feb 10, 2024 N/A pytest (>=6) + :pypi:`pytest-adaptavist` pytest plugin for generating test execution results within Jira Test Management (tm4j) Oct 13, 2022 N/A pytest (>=5.4.0) + :pypi:`pytest-adaptavist-fixed` pytest plugin for generating test execution results within Jira Test Management (tm4j) Nov 08, 2023 N/A pytest >=5.4.0 + :pypi:`pytest-addons-test` 用于测试pytest的插件 Aug 02, 2021 N/A pytest (>=6.2.4,<7.0.0) + :pypi:`pytest-adf` Pytest plugin for writing Azure Data Factory integration tests May 10, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-adf-azure-identity` Pytest plugin for writing Azure Data Factory integration tests Mar 06, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-ads-testplan` Azure DevOps Test Case reporting for pytest tests Sep 15, 2022 N/A N/A + :pypi:`pytest-affected` Nov 06, 2023 N/A N/A + :pypi:`pytest-agent` Service that exposes a REST API that can be used to interract remotely with Pytest. It is shipped with a dashboard that enables running tests in a more convenient way. Nov 25, 2021 N/A N/A + :pypi:`pytest-aggreport` pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. Mar 07, 2021 4 - Beta pytest (>=6.2.2) + :pypi:`pytest-ai1899` pytest plugin for connecting to ai1899 smart system stack Mar 13, 2024 5 - Production/Stable N/A + :pypi:`pytest-aio` Pytest plugin for testing async python code Apr 08, 2024 5 - Production/Stable pytest + :pypi:`pytest-aiofiles` pytest fixtures for writing aiofiles tests with pyfakefs May 14, 2017 5 - Production/Stable N/A + :pypi:`pytest-aiogram` May 06, 2023 N/A N/A + :pypi:`pytest-aiohttp` Pytest plugin for aiohttp support Sep 06, 2023 4 - Beta pytest >=6.1.0 + :pypi:`pytest-aiohttp-client` Pytest \`client\` fixture for the Aiohttp Jan 10, 2023 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-aiomoto` pytest-aiomoto Jun 24, 2023 N/A pytest (>=7.0,<8.0) + :pypi:`pytest-aioresponses` py.test integration for aioresponses Jul 29, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest May 01, 2023 5 - Production/Stable pytest>=6.1.0 + :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) + :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Mar 04, 2024 N/A pytest (>=6.0) + :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest + :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) + :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) + :pypi:`pytest-allure-collection` pytest plugin to collect allure markers without running any tests Apr 13, 2023 N/A pytest + :pypi:`pytest-allure-dsl` pytest plugin to test case doc string dls instructions Oct 25, 2020 4 - Beta pytest + :pypi:`pytest-allure-intersection` Oct 27, 2022 N/A pytest (<5) + :pypi:`pytest-allure-spec-coverage` The pytest plugin aimed to display test coverage of the specs(requirements) in Allure Oct 26, 2021 N/A pytest + :pypi:`pytest-alphamoon` Static code checks used at Alphamoon Dec 30, 2021 5 - Production/Stable pytest (>=3.5.0) + :pypi:`pytest-analyzer` this plugin allows to analyze tests in pytest project, collect test metadata and sync it with testomat.io TCM system Feb 21, 2024 N/A pytest <8.0.0,>=7.3.1 + :pypi:`pytest-android` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Feb 21, 2019 3 - Alpha pytest + :pypi:`pytest-anki` A pytest plugin for testing Anki add-ons Jul 31, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-annotate` pytest-annotate: Generate PyAnnotate annotations from your pytest tests. Jun 07, 2022 3 - Alpha pytest (<8.0.0,>=3.2.0) + :pypi:`pytest-ansible` Plugin for pytest to simplify calling ansible modules from tests or fixtures Jan 18, 2024 5 - Production/Stable pytest >=6 + :pypi:`pytest-ansible-playbook` Pytest fixture which runs given ansible playbook file. Mar 08, 2019 4 - Beta N/A + :pypi:`pytest-ansible-playbook-runner` Pytest fixture which runs given ansible playbook file. Dec 02, 2020 4 - Beta pytest (>=3.1.0) + :pypi:`pytest-ansible-units` A pytest plugin for running unit tests within an ansible collection Apr 14, 2022 N/A N/A + :pypi:`pytest-antilru` Bust functools.lru_cache when running pytest to avoid test pollution Jul 05, 2022 5 - Production/Stable pytest + :pypi:`pytest-anyio` The pytest anyio plugin is built into anyio. You don't need this package. Jun 29, 2021 N/A pytest + :pypi:`pytest-anything` Pytest fixtures to assert anything and something Jan 18, 2024 N/A pytest + :pypi:`pytest-aoc` Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures Dec 02, 2023 5 - Production/Stable pytest ; extra == 'test' + :pypi:`pytest-aoreporter` pytest report Jun 27, 2022 N/A N/A + :pypi:`pytest-api` An ASGI middleware to populate OpenAPI Specification examples from pytest functions May 12, 2022 N/A pytest (>=7.1.1,<8.0.0) + :pypi:`pytest-api-soup` Validate multiple endpoints with unit testing using a single source of truth. Aug 27, 2022 N/A N/A + :pypi:`pytest-apistellar` apistellar plugin for pytest. Jun 18, 2019 N/A N/A + :pypi:`pytest-appengine` AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A + :pypi:`pytest-appium` Pytest plugin for appium Dec 05, 2019 N/A N/A + :pypi:`pytest-approvaltests` A plugin to use approvaltests with pytest May 08, 2022 4 - Beta pytest (>=7.0.1) + :pypi:`pytest-approvaltests-geo` Extension for ApprovalTests.Python specific to geo data verification Feb 05, 2024 5 - Production/Stable pytest + :pypi:`pytest-archon` Rule your architecture like a real developer Dec 18, 2023 5 - Production/Stable pytest >=7.2 + :pypi:`pytest-argus` pyest results colection plugin Jun 24, 2021 5 - Production/Stable pytest (>=6.2.4) + :pypi:`pytest-arraydiff` pytest plugin to help with comparing array output from tests Nov 27, 2023 4 - Beta pytest >=4.6 + :pypi:`pytest-asgi-server` Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) + :pypi:`pytest-aspec` A rspec format reporter for pytest Dec 20, 2023 4 - Beta N/A + :pypi:`pytest-asptest` test Answer Set Programming programs Apr 28, 2018 4 - Beta N/A + :pypi:`pytest-assertcount` Plugin to count actual number of asserts in pytest Oct 23, 2022 N/A pytest (>=5.0.0) + :pypi:`pytest-assertions` Pytest Assertions Apr 27, 2022 N/A N/A + :pypi:`pytest-assertutil` pytest-assertutil May 10, 2019 N/A N/A + :pypi:`pytest-assert-utils` Useful assertion utilities for use with pytest Apr 14, 2022 3 - Alpha N/A + :pypi:`pytest-assume` A pytest plugin that allows multiple failures per test Jun 24, 2021 N/A pytest (>=2.7) + :pypi:`pytest-assurka` A pytest plugin for Assurka Studio Aug 04, 2022 N/A N/A + :pypi:`pytest-ast-back-to-python` A plugin for pytest devs to view how assertion rewriting recodes the AST Sep 29, 2019 4 - Beta N/A + :pypi:`pytest-asteroid` PyTest plugin for docker-based testing on database images Aug 15, 2022 N/A pytest (>=6.2.5,<8.0.0) + :pypi:`pytest-astropy` Meta-package containing dependencies for testing Sep 26, 2023 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-astropy-header` pytest plugin to add diagnostic information to the header of the test output Sep 06, 2022 3 - Alpha pytest (>=4.6) + :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest + :pypi:`pytest_async` pytest-async - Run your coroutine in event loop without decorator Feb 26, 2020 N/A N/A + :pypi:`pytest-async-generators` Pytest fixtures for async generators Jul 05, 2023 N/A N/A + :pypi:`pytest-asyncio` Pytest support for asyncio Mar 19, 2024 4 - Beta pytest <9,>=7.0.0 + :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 25, 2024 N/A N/A + :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) + :pypi:`pytest-async-mongodb` pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) + :pypi:`pytest-async-sqlalchemy` Database testing fixtures using the SQLAlchemy asyncio API Oct 07, 2021 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-atf-allure` 基于allure-pytest进行自定义 Nov 29, 2023 N/A pytest (>=7.4.2,<8.0.0) + :pypi:`pytest-atomic` Skip rest of tests if previous test failed. Nov 24, 2018 4 - Beta N/A + :pypi:`pytest-attrib` pytest plugin to select tests based on attributes similar to the nose-attrib plugin May 24, 2016 4 - Beta N/A + :pypi:`pytest-austin` Austin plugin for pytest Oct 11, 2020 4 - Beta N/A + :pypi:`pytest-autocap` automatically capture test & fixture stdout/stderr to files May 15, 2022 N/A pytest (<7.2,>=7.1.2) + :pypi:`pytest-autochecklog` automatically check condition and log all the checks Apr 25, 2015 4 - Beta N/A + :pypi:`pytest-automation` pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. May 20, 2022 N/A pytest (>=7.0.0) + :pypi:`pytest-automock` Pytest plugin for automatical mocks creation May 16, 2023 N/A pytest ; extra == 'dev' + :pypi:`pytest-auto-parametrize` pytest plugin: avoid repeating arguments in parametrize Oct 02, 2016 3 - Alpha N/A + :pypi:`pytest-autotest` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Aug 25, 2021 N/A pytest + :pypi:`pytest-aviator` Aviator's Flakybot pytest plugin that automatically reruns flaky tests. Nov 04, 2022 4 - Beta pytest + :pypi:`pytest-avoidance` Makes pytest skip tests that don not need rerunning May 23, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-aws` pytest plugin for testing AWS resource configurations Oct 04, 2017 4 - Beta N/A + :pypi:`pytest-aws-config` Protect your AWS credentials in unit tests May 28, 2021 N/A N/A + :pypi:`pytest-aws-fixtures` A series of fixtures to use in integration tests involving actual AWS services. Feb 02, 2024 N/A pytest (>=8.0.0,<9.0.0) + :pypi:`pytest-axe` pytest plugin for axe-selenium-python Nov 12, 2018 N/A pytest (>=3.0.0) + :pypi:`pytest-axe-playwright-snapshot` A pytest plugin that runs Axe-core on Playwright pages and takes snapshots of the results. Jul 25, 2023 N/A pytest + :pypi:`pytest-azure` Pytest utilities and mocks for Azure Jan 18, 2023 3 - Alpha pytest + :pypi:`pytest-azure-devops` Simplifies using azure devops parallel strategy (https://docs.microsoft.com/en-us/azure/devops/pipelines/test/parallel-testing-any-test-runner) with pytest. Jun 20, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-azurepipelines` Formatting PyTest output for Azure Pipelines UI Oct 06, 2023 5 - Production/Stable pytest (>=5.0.0) + :pypi:`pytest-bandit` A bandit plugin for pytest Feb 23, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-bandit-xayon` A bandit plugin for pytest Oct 17, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-base-url` pytest plugin for URL based testing Jan 31, 2024 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-bdd` BDD for pytest Mar 17, 2024 6 - Mature pytest (>=6.2.0) + :pypi:`pytest-bdd-html` pytest plugin to display BDD info in HTML test report Nov 22, 2022 3 - Alpha pytest (!=6.0.0,>=5.0) + :pypi:`pytest-bdd-ng` BDD for pytest Dec 31, 2023 4 - Beta pytest >=5.0 + :pypi:`pytest-bdd-report` A pytest-bdd plugin for generating useful and informative BDD test reports Feb 19, 2024 N/A pytest >=7.1.3 + :pypi:`pytest-bdd-splinter` Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) + :pypi:`pytest-bdd-web` A simple plugin to use with pytest Jan 02, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-bdd-wrappers` Feb 11, 2020 2 - Pre-Alpha N/A + :pypi:`pytest-beakerlib` A pytest plugin that reports test results to the BeakerLib framework Mar 17, 2017 5 - Production/Stable pytest + :pypi:`pytest-beartype` Pytest plugin to run your tests with beartype checking enabled. Jan 25, 2024 N/A pytest + :pypi:`pytest-bec-e2e` BEC pytest plugin for end-to-end tests Apr 19, 2024 3 - Alpha pytest + :pypi:`pytest-beds` Fixtures for testing Google Appengine (GAE) apps Jun 07, 2016 4 - Beta N/A + :pypi:`pytest-beeprint` use icdiff for better error messages in pytest assertions Jul 04, 2023 4 - Beta N/A + :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A + :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Oct 25, 2022 5 - Production/Stable pytest (>=3.8) + :pypi:`pytest-better-datadir` A small example package Mar 13, 2023 N/A N/A + :pypi:`pytest-better-parametrize` Better description of parametrized test cases Mar 05, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-bg-process` Pytest plugin to initialize background process Jan 24, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-bigchaindb` A BigchainDB plugin for pytest. Jan 24, 2022 4 - Beta N/A + :pypi:`pytest-bigquery-mock` Provides a mock fixture for python bigquery client Dec 28, 2022 N/A pytest (>=5.0) + :pypi:`pytest-bisect-tests` Find tests leaking state and affecting other Mar 25, 2024 N/A N/A + :pypi:`pytest-black` A pytest plugin to enable format checking with black Oct 05, 2020 4 - Beta N/A + :pypi:`pytest-black-multipy` Allow '--black' on older Pythons Jan 14, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' + :pypi:`pytest-black-ng` A pytest plugin to enable format checking with black Oct 20, 2022 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-blame` A pytest plugin helps developers to debug by providing useful commits history. May 04, 2019 N/A pytest (>=4.4.0) + :pypi:`pytest-blender` Blender Pytest plugin. Aug 10, 2023 N/A pytest ; extra == 'dev' + :pypi:`pytest-blink1` Pytest plugin to emit notifications via the Blink(1) RGB LED Jan 07, 2018 4 - Beta N/A + :pypi:`pytest-blockage` Disable network requests during a test run. Dec 21, 2021 N/A pytest + :pypi:`pytest-blocker` pytest plugin to mark a test as blocker and skip all other tests Sep 07, 2015 4 - Beta N/A + :pypi:`pytest-blue` A pytest plugin that adds a \`blue\` fixture for printing stuff in blue. Sep 05, 2022 N/A N/A + :pypi:`pytest-board` Local continuous test runner with pytest and watchdog. Jan 20, 2019 N/A N/A + :pypi:`pytest-boost-xml` Plugin for pytest to generate boost xml reports Nov 30, 2022 4 - Beta N/A + :pypi:`pytest-bootstrap` Mar 04, 2022 N/A N/A + :pypi:`pytest-bpdb` A py.test plug-in to enable drop to bpdb debugger on test failure. Jan 19, 2015 2 - Pre-Alpha N/A + :pypi:`pytest-bravado` Pytest-bravado automatically generates from OpenAPI specification client fixtures. Feb 15, 2022 N/A N/A + :pypi:`pytest-breakword` Use breakword with pytest Aug 04, 2021 N/A pytest (>=6.2.4,<7.0.0) + :pypi:`pytest-breed-adapter` A simple plugin to connect with breed-server Nov 07, 2018 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-briefcase` A pytest plugin for running tests on a Briefcase project. Jun 14, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-broadcaster` Pytest plugin to broadcast pytest output to various destinations Apr 06, 2024 3 - Alpha pytest + :pypi:`pytest-browser` A pytest plugin for console based browser test selection just after the collection phase Dec 10, 2016 3 - Alpha N/A + :pypi:`pytest-browsermob-proxy` BrowserMob proxy plugin for py.test. Jun 11, 2013 4 - Beta N/A + :pypi:`pytest_browserstack` Py.test plugin for BrowserStack Jan 27, 2016 4 - Beta N/A + :pypi:`pytest-browserstack-local` \`\`py.test\`\` plugin to run \`\`BrowserStackLocal\`\` in background. Feb 09, 2018 N/A N/A + :pypi:`pytest-budosystems` Budo Systems is a martial arts school management system. This module is the Budo Systems Pytest Plugin. May 07, 2023 3 - Alpha pytest + :pypi:`pytest-bug` Pytest plugin for marking tests as a bug Sep 23, 2023 5 - Production/Stable pytest >=7.1.0 + :pypi:`pytest-bugtong-tag` pytest-bugtong-tag is a plugin for pytest Jan 16, 2022 N/A N/A + :pypi:`pytest-bugzilla` py.test bugzilla integration plugin May 05, 2010 4 - Beta N/A + :pypi:`pytest-bugzilla-notifier` A plugin that allows you to execute create, update, and read information from BugZilla bugs Jun 15, 2018 4 - Beta pytest (>=2.9.2) + :pypi:`pytest-buildkite` Plugin for pytest that automatically publishes coverage and pytest report annotations to Buildkite. Jul 13, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-builtin-types` Nov 17, 2021 N/A pytest + :pypi:`pytest-bwrap` Run your tests in Bubblewrap sandboxes Feb 25, 2024 3 - Alpha N/A + :pypi:`pytest-cache` pytest plugin with mechanisms for caching across test runs Jun 04, 2013 3 - Alpha N/A + :pypi:`pytest-cache-assert` Cache assertion data to simplify regression testing of complex serializable data Aug 14, 2023 5 - Production/Stable pytest (>=6.0.0) + :pypi:`pytest-cagoule` Pytest plugin to only run tests affected by changes Jan 01, 2020 3 - Alpha N/A + :pypi:`pytest-cairo` Pytest support for cairo-lang and starknet Apr 17, 2022 N/A pytest + :pypi:`pytest-call-checker` Small pytest utility to easily create test doubles Oct 16, 2022 4 - Beta pytest (>=7.1.3,<8.0.0) + :pypi:`pytest-camel-collect` Enable CamelCase-aware pytest class collection Aug 02, 2020 N/A pytest (>=2.9) + :pypi:`pytest-canonical-data` A plugin which allows to compare results with canonical results, based on previous runs May 08, 2020 2 - Pre-Alpha pytest (>=3.5.0) + :pypi:`pytest-caprng` A plugin that replays pRNG state on failure. May 02, 2018 4 - Beta N/A + :pypi:`pytest-capture-deprecatedwarnings` pytest plugin to capture all deprecatedwarnings and put them in one file Apr 30, 2019 N/A N/A + :pypi:`pytest-capture-warnings` pytest plugin to capture all warnings and put them in one file of your choice May 03, 2022 N/A pytest + :pypi:`pytest-cases` Separate test code from test cases in pytest. Apr 04, 2024 5 - Production/Stable N/A + :pypi:`pytest-cassandra` Cassandra CCM Test Fixtures for pytest Nov 04, 2017 1 - Planning N/A + :pypi:`pytest-catchlog` py.test plugin to catch log messages. This is a fork of pytest-capturelog. Jan 24, 2016 4 - Beta pytest (>=2.6) + :pypi:`pytest-catch-server` Pytest plugin with server for catching HTTP requests. Dec 12, 2019 5 - Production/Stable N/A + :pypi:`pytest-celery` Pytest plugin for Celery Apr 11, 2024 4 - Beta N/A + :pypi:`pytest-cfg-fetcher` Pass config options to your unit tests. Feb 26, 2024 N/A N/A + :pypi:`pytest-chainmaker` pytest plugin for chainmaker Oct 15, 2021 N/A N/A + :pypi:`pytest-chalice` A set of py.test fixtures for AWS Chalice Jul 01, 2020 4 - Beta N/A + :pypi:`pytest-change-assert` 修改报错中文为英文 Oct 19, 2022 N/A N/A + :pypi:`pytest-change-demo` turn . into √,turn F into x Mar 02, 2022 N/A pytest + :pypi:`pytest-change-report` turn . into √,turn F into x Sep 14, 2020 N/A pytest + :pypi:`pytest-change-xds` turn . into √,turn F into x Apr 16, 2022 N/A pytest + :pypi:`pytest-chdir` A pytest fixture for changing current working directory Jan 28, 2020 N/A pytest (>=5.0.0,<6.0.0) + :pypi:`pytest-check` A pytest plugin that allows multiple failures per test. Jan 18, 2024 N/A pytest>=7.0.0 + :pypi:`pytest-checkdocs` check the README when running tests Mar 31, 2024 5 - Production/Stable pytest>=6; extra == "testing" + :pypi:`pytest-checkipdb` plugin to check if there are ipdb debugs left Dec 04, 2023 5 - Production/Stable pytest >=2.9.2 + :pypi:`pytest-check-library` check your missing library Jul 17, 2022 N/A N/A + :pypi:`pytest-check-libs` check your missing library Jul 17, 2022 N/A N/A + :pypi:`pytest-check-links` Check links in files Jul 29, 2020 N/A pytest<9,>=7.0 + :pypi:`pytest-checklist` Pytest plugin to track and report unit/function coverage. Mar 12, 2024 N/A N/A + :pypi:`pytest-check-mk` pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest + :pypi:`pytest-check-requirements` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A + :pypi:`pytest-ch-framework` My pytest framework Apr 17, 2024 N/A pytest==8.0.1 + :pypi:`pytest-chic-report` A pytest plugin to send a report and printing summary of tests. Jan 31, 2023 5 - Production/Stable N/A + :pypi:`pytest-choose` Provide the pytest with the ability to collect use cases based on rules in text files Feb 04, 2024 N/A pytest >=7.0.0 + :pypi:`pytest-chunks` Run only a chunk of your test suite Jul 05, 2022 N/A pytest (>=6.0.0) + :pypi:`pytest_cid` Compare data structures containing matching CIDs of different versions and encoding Sep 01, 2023 4 - Beta pytest >= 5.0, < 7.0 + :pypi:`pytest-circleci` py.test plugin for CircleCI May 03, 2019 N/A N/A + :pypi:`pytest-circleci-parallelized` Parallelize pytest across CircleCI workers. Oct 20, 2022 N/A N/A + :pypi:`pytest-circleci-parallelized-rjp` Parallelize pytest across CircleCI workers. Jun 21, 2022 N/A pytest + :pypi:`pytest-ckan` Backport of CKAN 2.9 pytest plugin and fixtures to CAKN 2.8 Apr 28, 2020 4 - Beta pytest + :pypi:`pytest-clarity` A plugin providing an alternative, colourful diff output for failing assertions. Jun 11, 2021 N/A N/A + :pypi:`pytest-cldf` Easy quality control for CLDF datasets using pytest Nov 07, 2022 N/A pytest (>=3.6) + :pypi:`pytest_cleanup` Automated, comprehensive and well-organised pytest test cases. Jan 28, 2020 N/A N/A + :pypi:`pytest-cleanuptotal` A cleanup plugin for pytest Mar 19, 2024 5 - Production/Stable N/A + :pypi:`pytest-clerk` A set of pytest fixtures to help with integration testing with Clerk. Apr 19, 2024 N/A pytest<9.0.0,>=8.0.0 + :pypi:`pytest-click` Pytest plugin for Click Feb 11, 2022 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-cli-fixtures` Automatically register fixtures for custom CLI arguments Jul 28, 2022 N/A pytest (~=7.0) + :pypi:`pytest-clld` Jul 06, 2022 N/A pytest (>=3.6) + :pypi:`pytest-cloud` Distributed tests planner plugin for pytest testing framework. Oct 05, 2020 6 - Mature N/A + :pypi:`pytest-cloudflare-worker` pytest plugin for testing cloudflare workers Mar 30, 2021 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-cloudist` Distribute tests to cloud machines without fuss Sep 02, 2022 4 - Beta pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-cmake` Provide CMake module for Pytest Mar 18, 2024 N/A pytest<9,>=4 + :pypi:`pytest-cmake-presets` Execute CMake Presets via pytest Dec 26, 2022 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-cobra` PyTest plugin for testing Smart Contracts for Ethereum blockchain. Jun 29, 2019 3 - Alpha pytest (<4.0.0,>=3.7.1) + :pypi:`pytest_codeblocks` Test code blocks in your READMEs Sep 17, 2023 5 - Production/Stable pytest >= 7.0.0 + :pypi:`pytest-codecarbon` Pytest plugin for measuring carbon emissions Jun 15, 2022 N/A pytest + :pypi:`pytest-codecheckers` pytest plugin to add source code sanity checks (pep8 and friends) Feb 13, 2010 N/A N/A + :pypi:`pytest-codecov` Pytest plugin for uploading pytest-cov results to codecov.io Nov 29, 2022 4 - Beta pytest (>=4.6.0) + :pypi:`pytest-codegen` Automatically create pytest test signatures Aug 23, 2020 2 - Pre-Alpha N/A + :pypi:`pytest-codeowners` Pytest plugin for selecting tests by GitHub CODEOWNERS. Mar 30, 2022 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-codestyle` pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A + :pypi:`pytest-codspeed` Pytest plugin to create CodSpeed benchmarks Mar 19, 2024 5 - Production/Stable pytest>=3.8 + :pypi:`pytest-collect-appoint-info` set your encoding Aug 03, 2023 N/A pytest + :pypi:`pytest-collect-formatter` Formatter for pytest collect output Mar 29, 2021 5 - Production/Stable N/A + :pypi:`pytest-collect-formatter2` Formatter for pytest collect output May 31, 2021 5 - Production/Stable N/A + :pypi:`pytest-collect-interface-info-plugin` Get executed interface information in pytest interface automation framework Sep 25, 2023 4 - Beta N/A + :pypi:`pytest-collector` Python package for collecting pytest. Aug 02, 2022 N/A pytest (>=7.0,<8.0) + :pypi:`pytest-collect-pytest-interinfo` A simple plugin to use with pytest Sep 26, 2023 4 - Beta N/A + :pypi:`pytest-colordots` Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A + :pypi:`pytest-commander` An interactive GUI test runner for PyTest Aug 17, 2021 N/A pytest (<7.0.0,>=6.2.4) + :pypi:`pytest-common-subject` pytest framework for testing different aspects of a common method May 15, 2022 N/A pytest (>=3.6,<8) + :pypi:`pytest-compare` pytest plugin for comparing call arguments. Jun 22, 2023 5 - Production/Stable N/A + :pypi:`pytest-concurrent` Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-config` Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A + :pypi:`pytest-confluence-report` Package stands for pytest plugin to upload results into Confluence page. Apr 17, 2022 N/A N/A + :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts May 31, 2023 4 - Beta pytest (>=4.0.0) + :pypi:`pytest-consul` pytest plugin with fixtures for testing consul aware apps Nov 24, 2018 3 - Alpha pytest + :pypi:`pytest-container` Pytest fixtures for writing container based tests Apr 10, 2024 4 - Beta pytest>=3.10 + :pypi:`pytest-contextfixture` Define pytest fixtures as context managers. Mar 12, 2013 4 - Beta N/A + :pypi:`pytest-contexts` A plugin to run tests written with the Contexts framework using pytest May 19, 2021 4 - Beta N/A + :pypi:`pytest-cookies` The pytest plugin for your Cookiecutter templates. 🍪 Mar 22, 2023 5 - Production/Stable pytest (>=3.9.0) + :pypi:`pytest-copie` The pytest plugin for your copier templates 📒 Jan 27, 2024 3 - Alpha pytest + :pypi:`pytest-copier` A pytest plugin to help testing Copier templates Dec 11, 2023 4 - Beta pytest>=7.3.2 + :pypi:`pytest-couchdbkit` py.test extension for per-test couchdb databases using couchdbkit Apr 17, 2012 N/A N/A + :pypi:`pytest-count` count erros and send email Jan 12, 2018 4 - Beta N/A + :pypi:`pytest-cov` Pytest plugin for measuring coverage. Mar 24, 2024 5 - Production/Stable pytest>=4.6 + :pypi:`pytest-cover` Pytest plugin for measuring coverage. Forked from \`pytest-cov\`. Aug 01, 2015 5 - Production/Stable N/A + :pypi:`pytest-coverage` Jun 17, 2015 N/A N/A + :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jun 28, 2023 4 - Beta N/A + :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Apr 15, 2024 N/A pytest<8.0.0,>=7.1.2 + :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' + :pypi:`pytest_covid` Too many faillure, less tests. Jun 24, 2020 N/A N/A + :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Nov 01, 2023 5 - Production/Stable pytest >=7.0 + :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types Mar 14, 2024 N/A N/A + :pypi:`pytest-cqase` Custom qase pytest plugin Aug 22, 2022 N/A pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-cram` Run cram tests with pytest. Aug 08, 2020 N/A N/A + :pypi:`pytest-crate` Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) + :pypi:`pytest-crayons` A pytest plugin for colorful print statements Oct 08, 2023 N/A pytest + :pypi:`pytest-create` pytest-create Feb 15, 2023 1 - Planning N/A + :pypi:`pytest-cricri` A Cricri plugin for pytest. Jan 27, 2018 N/A pytest + :pypi:`pytest-crontab` add crontab task in crontab Dec 09, 2019 N/A N/A + :pypi:`pytest-csv` CSV output for pytest. Apr 22, 2021 N/A pytest (>=6.0) + :pypi:`pytest-csv-params` Pytest plugin for Test Case Parametrization with CSV files Jul 01, 2023 5 - Production/Stable pytest (>=7.4.0,<8.0.0) + :pypi:`pytest-curio` Pytest support for curio. Oct 07, 2020 N/A N/A + :pypi:`pytest-curl-report` pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A + :pypi:`pytest-custom-concurrency` Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A + :pypi:`pytest-custom-exit-code` Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) + :pypi:`pytest-custom-nodeid` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 07, 2021 N/A N/A + :pypi:`pytest-custom-report` Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest + :pypi:`pytest-custom-scheduling` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 01, 2021 N/A N/A + :pypi:`pytest-cython` A plugin for testing Cython extension modules Apr 05, 2024 5 - Production/Stable pytest>=8 + :pypi:`pytest-cython-collect` Jun 17, 2022 N/A pytest + :pypi:`pytest-darker` A pytest plugin for checking of modified code using Darker Feb 25, 2024 N/A pytest <7,>=6.0.1 + :pypi:`pytest-dash` pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A + :pypi:`pytest-dashboard` Apr 18, 2024 N/A pytest<8.0.0,>=7.4.3 + :pypi:`pytest-data` Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A + :pypi:`pytest-databases` Reusable database fixtures for any and all databases. Apr 19, 2024 4 - Beta pytest + :pypi:`pytest-databricks` Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest + :pypi:`pytest-datadir` pytest plugin for test data directories and files Oct 03, 2023 5 - Production/Stable pytest >=5.0 + :pypi:`pytest-datadir-mgr` Manager for test data: downloads, artifact caching, and a tmpdir context. Apr 06, 2023 5 - Production/Stable pytest (>=7.1) + :pypi:`pytest-datadir-ng` Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. Dec 25, 2019 5 - Production/Stable pytest + :pypi:`pytest-datadir-nng` Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. Nov 09, 2022 5 - Production/Stable pytest (>=7.0.0,<8.0.0) + :pypi:`pytest-data-extractor` A pytest plugin to extract relevant metadata about tests into an external file (currently only json support) Jul 19, 2022 N/A pytest (>=7.0.1) + :pypi:`pytest-data-file` Fixture "data" and "case_data" for test from yaml file Dec 04, 2019 N/A N/A + :pypi:`pytest-datafiles` py.test plugin to create a 'tmp_path' containing predefined files/directories. Feb 24, 2023 5 - Production/Stable pytest (>=3.6) + :pypi:`pytest-datafixtures` Data fixtures for pytest made simple Dec 05, 2020 5 - Production/Stable N/A + :pypi:`pytest-data-from-files` pytest plugin to provide data from files loaded automatically Oct 13, 2021 4 - Beta pytest + :pypi:`pytest-dataplugin` A pytest plugin for managing an archive of test data. Sep 16, 2017 1 - Planning N/A + :pypi:`pytest-datarecorder` A py.test plugin recording and comparing test output. Feb 15, 2024 5 - Production/Stable pytest + :pypi:`pytest-dataset` Plugin for loading different datasets for pytest by prefix from json or yaml files Sep 01, 2023 5 - Production/Stable N/A + :pypi:`pytest-data-suites` Class-based pytest parametrization Apr 06, 2024 N/A pytest<9.0,>=6.0 + :pypi:`pytest-datatest` A pytest plugin for test driven data-wrangling (this is the development version of datatest's pytest integration). Oct 15, 2020 4 - Beta pytest (>=3.3) + :pypi:`pytest-db` Session scope fixture "db" for mysql query or change Dec 04, 2019 N/A N/A + :pypi:`pytest-dbfixtures` Databases fixtures plugin for py.test. Dec 07, 2016 4 - Beta N/A + :pypi:`pytest-db-plugin` Nov 27, 2021 N/A pytest (>=5.0) + :pypi:`pytest-dbt` Unit test dbt models with standard python tooling Jun 08, 2023 2 - Pre-Alpha pytest (>=7.0.0,<8.0.0) + :pypi:`pytest-dbt-adapter` A pytest plugin for testing dbt adapter plugins Nov 24, 2021 N/A pytest (<7,>=6) + :pypi:`pytest-dbt-conventions` A pytest plugin for linting a dbt project's conventions Mar 02, 2022 N/A pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-dbt-core` Pytest extension for dbt. Aug 25, 2023 N/A pytest >=6.2.5 ; extra == 'test' + :pypi:`pytest-dbt-postgres` Pytest tooling to unittest DBT & Postgres models Jan 02, 2024 N/A pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-dbus-notification` D-BUS notifications for pytest results. Mar 05, 2014 5 - Production/Stable N/A + :pypi:`pytest-dbx` Pytest plugin to run unit tests for dbx (Databricks CLI extensions) related code Nov 29, 2022 N/A pytest (>=7.1.3,<8.0.0) + :pypi:`pytest-dc` Manages Docker containers during your integration tests Aug 16, 2023 5 - Production/Stable pytest >=3.3 + :pypi:`pytest-deadfixtures` A simple plugin to list unused fixtures in pytest Jul 23, 2020 5 - Production/Stable N/A + :pypi:`pytest-deduplicate` Identifies duplicate unit tests Aug 12, 2023 4 - Beta pytest + :pypi:`pytest-deepcov` deepcov Mar 30, 2021 N/A N/A + :pypi:`pytest-defer` Aug 24, 2021 N/A N/A + :pypi:`pytest-demo-plugin` pytest示例插件 May 15, 2021 N/A N/A + :pypi:`pytest-dependency` Manage dependencies of tests Dec 31, 2023 4 - Beta N/A + :pypi:`pytest-depends` Tests that depend on other tests Apr 05, 2020 5 - Production/Stable pytest (>=3) + :pypi:`pytest-deprecate` Mark tests as testing a deprecated feature with a warning note. Jul 01, 2019 N/A N/A + :pypi:`pytest-describe` Describe-style plugin for pytest Feb 10, 2024 5 - Production/Stable pytest <9,>=4.6 + :pypi:`pytest-describe-it` plugin for rich text descriptions Jul 19, 2019 4 - Beta pytest + :pypi:`pytest-deselect-if` A plugin to deselect pytests tests rather than using skipif Mar 24, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-devpi-server` DevPI server fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-dhos` Common fixtures for pytest in DHOS services and libraries Sep 07, 2022 N/A N/A + :pypi:`pytest-diamond` pytest plugin for diamond Aug 31, 2015 4 - Beta N/A + :pypi:`pytest-dicom` pytest plugin to provide DICOM fixtures Dec 19, 2018 3 - Alpha pytest + :pypi:`pytest-dictsdiff` Jul 26, 2019 N/A N/A + :pypi:`pytest-diff` A simple plugin to use with pytest Mar 30, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-diffeo` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A + :pypi:`pytest-diff-selector` Get tests affected by code changes (using git) Feb 24, 2022 4 - Beta pytest (>=6.2.2) ; extra == 'all' + :pypi:`pytest-difido` PyTest plugin for generating Difido reports Oct 23, 2022 4 - Beta pytest (>=4.0.0) + :pypi:`pytest-dir-equal` pytest-dir-equals is a pytest plugin providing helpers to assert directories equality allowing golden testing Dec 11, 2023 4 - Beta pytest>=7.3.2 + :pypi:`pytest-disable` pytest plugin to disable a test and skip it from testrun Sep 10, 2015 4 - Beta N/A + :pypi:`pytest-disable-plugin` Disable plugins per test Feb 28, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-discord` A pytest plugin to notify test results to a Discord channel. Oct 18, 2023 4 - Beta pytest !=6.0.0,<8,>=3.3.2 + :pypi:`pytest-discover` Pytest plugin to record discovered tests in a file Mar 26, 2024 N/A pytest + :pypi:`pytest-django` A Django plugin for pytest. Jan 30, 2024 5 - Production/Stable pytest >=7.0.0 + :pypi:`pytest-django-ahead` A Django plugin for pytest. Oct 27, 2016 5 - Production/Stable pytest (>=2.9) + :pypi:`pytest-djangoapp` Nice pytest plugin to help you with Django pluggable application testing. May 19, 2023 4 - Beta pytest + :pypi:`pytest-django-cache-xdist` A djangocachexdist plugin for pytest May 12, 2020 4 - Beta N/A + :pypi:`pytest-django-casperjs` Integrate CasperJS with your django tests as a pytest fixture. Mar 15, 2015 2 - Pre-Alpha N/A + :pypi:`pytest-django-class` A pytest plugin for running django in class-scoped fixtures Aug 08, 2023 4 - Beta N/A + :pypi:`pytest-django-docker-pg` Jan 30, 2024 5 - Production/Stable pytest <8.0.0,>=7.0.0 + :pypi:`pytest-django-dotenv` Pytest plugin used to setup environment variables with django-dotenv Nov 26, 2019 4 - Beta pytest (>=2.6.0) + :pypi:`pytest-django-factories` Factories for your Django models that can be used as Pytest fixtures. Nov 12, 2020 4 - Beta N/A + :pypi:`pytest-django-filefield` Replaces FileField.storage with something you can patch globally. May 09, 2022 5 - Production/Stable pytest >= 5.2 + :pypi:`pytest-django-gcir` A Django plugin for pytest. Mar 06, 2018 5 - Production/Stable N/A + :pypi:`pytest-django-haystack` Cleanup your Haystack indexes between tests Sep 03, 2017 5 - Production/Stable pytest (>=2.3.4) + :pypi:`pytest-django-ifactory` A model instance factory for pytest-django Aug 27, 2023 5 - Production/Stable N/A + :pypi:`pytest-django-lite` The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A + :pypi:`pytest-django-liveserver-ssl` Jan 20, 2022 3 - Alpha N/A + :pypi:`pytest-django-model` A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A + :pypi:`pytest-django-ordering` A pytest plugin for preserving the order in which Django runs tests. Jul 25, 2019 5 - Production/Stable pytest (>=2.3.0) + :pypi:`pytest-django-queries` Generate performance reports from your django database performance tests. Mar 01, 2021 N/A N/A + :pypi:`pytest-djangorestframework` A djangorestframework plugin for pytest Aug 11, 2019 4 - Beta N/A + :pypi:`pytest-django-rq` A pytest plugin to help writing unit test for django-rq Apr 13, 2020 4 - Beta N/A + :pypi:`pytest-django-sqlcounts` py.test plugin for reporting the number of SQLs executed per django testcase. Jun 16, 2015 4 - Beta N/A + :pypi:`pytest-django-testing-postgresql` Use a temporary PostgreSQL database with pytest-django Jan 31, 2022 4 - Beta N/A + :pypi:`pytest-doc` A documentation plugin for py.test. Jun 28, 2015 5 - Production/Stable N/A + :pypi:`pytest-docfiles` pytest plugin to test codeblocks in your documentation. Dec 22, 2021 4 - Beta pytest (>=3.7.0) + :pypi:`pytest-docgen` An RST Documentation Generator for pytest-based test suites Apr 17, 2020 N/A N/A + :pypi:`pytest-docker` Simple pytest fixtures for Docker and Docker Compose based tests Feb 02, 2024 N/A pytest <9.0,>=4.0 + :pypi:`pytest-docker-apache-fixtures` Pytest fixtures for testing with apache2 (httpd). Feb 16, 2022 4 - Beta pytest + :pypi:`pytest-docker-butla` Jun 16, 2019 3 - Alpha N/A + :pypi:`pytest-dockerc` Run, manage and stop Docker Compose project from Docker API Oct 09, 2020 5 - Production/Stable pytest (>=3.0) + :pypi:`pytest-docker-compose` Manages Docker containers during your integration tests Jan 26, 2021 5 - Production/Stable pytest (>=3.3) + :pypi:`pytest-docker-compose-v2` Manages Docker containers during your integration tests Feb 28, 2024 4 - Beta pytest<8,>=7.2.2 + :pypi:`pytest-docker-db` A plugin to use docker databases for pytests Mar 20, 2021 5 - Production/Stable pytest (>=3.1.1) + :pypi:`pytest-docker-fixtures` pytest docker fixtures Apr 03, 2024 3 - Alpha N/A + :pypi:`pytest-docker-git-fixtures` Pytest fixtures for testing with git scm. Feb 09, 2022 4 - Beta pytest + :pypi:`pytest-docker-haproxy-fixtures` Pytest fixtures for testing with haproxy. Feb 09, 2022 4 - Beta pytest + :pypi:`pytest-docker-pexpect` pytest plugin for writing functional tests with pexpect and docker Jan 14, 2019 N/A pytest + :pypi:`pytest-docker-postgresql` A simple plugin to use with pytest Sep 24, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-docker-py` Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. Nov 27, 2018 N/A pytest (==4.0.0) + :pypi:`pytest-docker-registry-fixtures` Pytest fixtures for testing with docker registries. Apr 08, 2022 4 - Beta pytest + :pypi:`pytest-docker-service` pytest plugin to start docker container Jan 03, 2024 3 - Alpha pytest (>=7.1.3) + :pypi:`pytest-docker-squid-fixtures` Pytest fixtures for testing with squid. Feb 09, 2022 4 - Beta pytest + :pypi:`pytest-docker-tools` Docker integration tests for pytest Feb 17, 2022 4 - Beta pytest (>=6.0.1) + :pypi:`pytest-docs` Documentation tool for pytest Nov 11, 2018 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-docstyle` pytest plugin to run pydocstyle Mar 23, 2020 3 - Alpha N/A + :pypi:`pytest-doctest-custom` A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A + :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A + :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) + :pypi:`pytest-doctest-mkdocstrings` Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) Mar 02, 2024 N/A pytest + :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Mar 10, 2024 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A + :pypi:`pytest-dogu-sdk` pytest plugin for the Dogu Dec 14, 2023 N/A N/A + :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) + :pypi:`pytest-donde` record pytest session characteristics per test item (coverage and duration) into a persistent file and use them in your own plugin or script. Oct 01, 2023 4 - Beta pytest >=7.3.1 + :pypi:`pytest-doorstop` A pytest plugin for adding test results into doorstop items. Jun 09, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-dotenv` A py.test plugin that parses environment files before running tests Jun 16, 2020 4 - Beta pytest (>=5.0.0) + :pypi:`pytest-dot-only-pkcopley` A Pytest marker for only running a single test Oct 27, 2023 N/A N/A + :pypi:`pytest-draw` Pytest plugin for randomly selecting a specific number of tests Mar 21, 2023 3 - Alpha pytest + :pypi:`pytest-drf` A Django REST framework plugin for pytest. Jul 12, 2022 5 - Production/Stable pytest (>=3.7) + :pypi:`pytest-drivings` Tool to allow webdriver automation to be ran locally or remotely Jan 13, 2021 N/A N/A + :pypi:`pytest-drop-dup-tests` A Pytest plugin to drop duplicated tests during collection Mar 04, 2024 5 - Production/Stable pytest >=7 + :pypi:`pytest-dryrun` A Pytest plugin to ignore tests during collection without reporting them in the test summary. Jul 18, 2023 5 - Production/Stable pytest (>=7.4.0,<8.0.0) + :pypi:`pytest-dummynet` A py.test plugin providing access to a dummynet. Dec 15, 2021 5 - Production/Stable pytest + :pypi:`pytest-dump2json` A pytest plugin for dumping test results to json. Jun 29, 2015 N/A N/A + :pypi:`pytest-duration-insights` Jun 25, 2021 N/A N/A + :pypi:`pytest-durations` Pytest plugin reporting fixtures and test functions execution time. Apr 22, 2022 5 - Production/Stable pytest (>=4.6) + :pypi:`pytest-dynamicrerun` A pytest plugin to rerun tests dynamically based off of test outcome and output. Aug 15, 2020 4 - Beta N/A + :pypi:`pytest-dynamodb` DynamoDB fixtures for pytest Mar 12, 2024 5 - Production/Stable pytest + :pypi:`pytest-easy-addoption` pytest-easy-addoption: Easy way to work with pytest addoption Jan 22, 2020 N/A N/A + :pypi:`pytest-easy-api` A package to prevent Dependency Confusion attacks against Yandex. Feb 16, 2024 N/A N/A + :pypi:`pytest-easyMPI` Package that supports mpi tests in pytest Oct 21, 2020 N/A N/A + :pypi:`pytest-easyread` pytest plugin that makes terminal printouts of the reports easier to read Nov 17, 2017 N/A N/A + :pypi:`pytest-easy-server` Pytest plugin for easy testing against servers May 01, 2021 4 - Beta pytest (<5.0.0,>=4.3.1) ; python_version < "3.5" + :pypi:`pytest-ebics-sandbox` A pytest plugin for testing against an EBICS sandbox server. Requires docker. Aug 15, 2022 N/A N/A + :pypi:`pytest-ec2` Pytest execution on EC2 instance Oct 22, 2019 3 - Alpha N/A + :pypi:`pytest-echo` pytest plugin with mechanisms for echoing environment variables, package version and generic attributes Dec 05, 2023 5 - Production/Stable pytest >=2.2 + :pypi:`pytest-ekstazi` Pytest plugin to select test using Ekstazi algorithm Sep 10, 2022 N/A pytest + :pypi:`pytest-elasticsearch` Elasticsearch fixtures and fixture factories for Pytest. Mar 15, 2024 5 - Production/Stable pytest >=7.0 + :pypi:`pytest-elements` Tool to help automate user interfaces Jan 13, 2021 N/A pytest (>=5.4,<6.0) + :pypi:`pytest-eliot` An eliot plugin for pytest. Aug 31, 2022 1 - Planning pytest (>=5.4.0) + :pypi:`pytest-elk-reporter` A simple plugin to use with pytest Apr 04, 2024 4 - Beta pytest>=3.5.0 + :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest + :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Apr 09, 2024 5 - Production/Stable pytest>=7.0 + :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-wokwi` Make pytest-embedded plugin work with the Wokwi CLI. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embrace` 💝 Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. Mar 25, 2023 N/A pytest (>=7.0,<8.0) + :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) + :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Apr 09, 2023 4 - Beta pytest (==7.0.1) + :pypi:`pytest-enabler` Enable installed pytest plugins Mar 21, 2024 5 - Production/Stable pytest>=6; extra == "testing" + :pypi:`pytest-encode` set your encoding and logger Nov 06, 2021 N/A N/A + :pypi:`pytest-encode-kane` set your encoding and logger Nov 16, 2021 N/A pytest + :pypi:`pytest-encoding` set your encoding and logger Aug 11, 2023 N/A pytest + :pypi:`pytest_energy_reporter` An energy estimation reporter for pytest Mar 28, 2024 3 - Alpha pytest<9.0.0,>=8.1.1 + :pypi:`pytest-enhanced-reports` Enhanced test reports for pytest Dec 15, 2022 N/A N/A + :pypi:`pytest-enhancements` Improvements for pytest (rejected upstream) Oct 30, 2019 4 - Beta N/A + :pypi:`pytest-env` pytest plugin that allows you to add environment variables. Nov 28, 2023 5 - Production/Stable pytest>=7.4.3 + :pypi:`pytest-envfiles` A py.test plugin that parses environment files before running tests Oct 08, 2015 3 - Alpha N/A + :pypi:`pytest-env-info` Push information about the running pytest into envvars Nov 25, 2017 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-environment` Pytest Environment Mar 17, 2024 1 - Planning N/A + :pypi:`pytest-envraw` py.test plugin that allows you to add environment variables. Aug 27, 2020 4 - Beta pytest (>=2.6.0) + :pypi:`pytest-envvars` Pytest plugin to validate use of envvars on your tests Jun 13, 2020 5 - Production/Stable pytest (>=3.0.0) + :pypi:`pytest-env-yaml` Apr 02, 2019 N/A N/A + :pypi:`pytest-eradicate` pytest plugin to check for commented out code Sep 08, 2020 N/A pytest (>=2.4.2) + :pypi:`pytest_erp` py.test plugin to send test info to report portal dynamically Jan 13, 2015 N/A N/A + :pypi:`pytest-error-for-skips` Pytest plugin to treat skipped tests a test failure Dec 19, 2019 4 - Beta pytest (>=4.6) + :pypi:`pytest-eth` PyTest plugin for testing Smart Contracts for Ethereum Virtual Machine (EVM). Aug 14, 2020 1 - Planning N/A + :pypi:`pytest-ethereum` pytest-ethereum: Pytest library for ethereum projects. Jun 24, 2019 3 - Alpha pytest (==3.3.2); extra == 'dev' + :pypi:`pytest-eucalyptus` Pytest Plugin for BDD Jun 28, 2022 N/A pytest (>=4.2.0) + :pypi:`pytest-eventlet` Applies eventlet monkey-patch as a pytest plugin. Oct 04, 2021 N/A pytest ; extra == 'dev' + :pypi:`pytest-evm` The testing package containing tools to test Web3-based projects Apr 20, 2024 4 - Beta pytest<9.0.0,>=8.1.1 + :pypi:`pytest_exact_fixtures` Parse queries in Lucene and Elasticsearch syntaxes Feb 04, 2019 N/A N/A + :pypi:`pytest-examples` Pytest plugin for testing examples in docstrings and markdown files. Jul 11, 2023 4 - Beta pytest>=7 + :pypi:`pytest-exasol-itde` Feb 15, 2024 N/A pytest (>=7,<9) + :pypi:`pytest-excel` pytest plugin for generating excel reports Sep 14, 2023 5 - Production/Stable N/A + :pypi:`pytest-exceptional` Better exceptions Mar 16, 2017 4 - Beta N/A + :pypi:`pytest-exception-script` Walk your code through exception script to check it's resiliency to failures. Aug 04, 2020 3 - Alpha pytest + :pypi:`pytest-executable` pytest plugin for testing executables Oct 07, 2023 N/A pytest <8,>=5 + :pypi:`pytest-execution-timer` A timer for the phases of Pytest's execution. Dec 24, 2021 4 - Beta N/A + :pypi:`pytest-exit-code` A pytest plugin that overrides the built-in exit codes to retain more information about the test results. Feb 23, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-expect` py.test plugin to store test expectations and mark tests based on them Apr 21, 2016 4 - Beta N/A + :pypi:`pytest-expectdir` A pytest plugin to provide initial/expected directories, and check a test transforms the initial directory to the expected one Mar 19, 2023 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-expecter` Better testing with expecter and pytest. Sep 18, 2022 5 - Production/Stable N/A + :pypi:`pytest-expectr` This plugin is used to expect multiple assert using pytest framework. Oct 05, 2018 N/A pytest (>=2.4.2) + :pypi:`pytest-expect-test` A fixture to support expect tests in pytest Apr 10, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-experiments` A pytest plugin to help developers of research-oriented software projects keep track of the results of their numerical experiments. Dec 13, 2021 4 - Beta pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-explicit` A Pytest plugin to ignore certain marked tests by default Jun 15, 2021 5 - Production/Stable pytest + :pypi:`pytest-exploratory` Interactive console for pytest. Aug 18, 2023 N/A pytest (>=6.2) + :pypi:`pytest-explorer` terminal ui for exploring and running tests Aug 01, 2023 N/A N/A + :pypi:`pytest-ext` pytest plugin for automation test Mar 31, 2024 N/A pytest>=5.3 + :pypi:`pytest-extensions` A collection of helpers for pytest to ease testing Aug 17, 2022 4 - Beta pytest ; extra == 'testing' + :pypi:`pytest-external-blockers` a special outcome for tests that are blocked for external reasons Oct 05, 2021 N/A pytest + :pypi:`pytest_extra` Some helpers for writing tests with pytest. Aug 14, 2014 N/A N/A + :pypi:`pytest-extra-durations` A pytest plugin to get durations on a per-function basis and per module basis. Apr 21, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-extra-markers` Additional pytest markers to dynamically enable/disable tests viia CLI flags Mar 05, 2023 4 - Beta pytest + :pypi:`pytest-fabric` Provides test utilities to run fabric task tests by using docker containers Sep 12, 2018 5 - Production/Stable N/A + :pypi:`pytest-factor` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A + :pypi:`pytest-factory` Use factories for test setup with py.test Sep 06, 2020 3 - Alpha pytest (>4.3) + :pypi:`pytest-factoryboy` Factory Boy support for pytest. Mar 05, 2024 6 - Mature pytest (>=6.2) + :pypi:`pytest-factoryboy-fixtures` Generates pytest fixtures that allow the use of type hinting Jun 25, 2020 N/A N/A + :pypi:`pytest-factoryboy-state` Simple factoryboy random state management Mar 22, 2022 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-failed-screen-record` Create a video of the screen when pytest fails Jan 05, 2023 4 - Beta pytest (>=7.1.2d,<8.0.0) + :pypi:`pytest-failed-screenshot` Test case fails,take a screenshot,save it,attach it to the allure Apr 21, 2021 N/A N/A + :pypi:`pytest-failed-to-verify` A pytest plugin that helps better distinguishing real test failures from setup flakiness. Aug 08, 2019 5 - Production/Stable pytest (>=4.1.0) + :pypi:`pytest-fail-slow` Fail tests that take too long to run Feb 11, 2024 N/A pytest>=7.0 + :pypi:`pytest-faker` Faker integration with the pytest framework. Dec 19, 2016 6 - Mature N/A + :pypi:`pytest-falcon` Pytest helpers for Falcon. Sep 07, 2016 4 - Beta N/A + :pypi:`pytest-falcon-client` A package to prevent Dependency Confusion attacks against Yandex. Feb 21, 2024 N/A N/A + :pypi:`pytest-fantasy` Pytest plugin for Flask Fantasy Framework Mar 14, 2019 N/A N/A + :pypi:`pytest-fastapi` Dec 27, 2020 N/A N/A + :pypi:`pytest-fastapi-deps` A fixture which allows easy replacement of fastapi dependencies for testing Jul 20, 2022 5 - Production/Stable pytest + :pypi:`pytest-fastest` Use SCM and coverage to run only needed tests Oct 04, 2023 4 - Beta pytest (>=4.4) + :pypi:`pytest-fast-first` Pytest plugin that runs fast tests first Jan 19, 2023 3 - Alpha pytest + :pypi:`pytest-faulthandler` py.test plugin that activates the fault handler module for tests (dummy package) Jul 04, 2019 6 - Mature pytest (>=5.0) + :pypi:`pytest-fauxfactory` Integration of fauxfactory into pytest. Dec 06, 2017 5 - Production/Stable pytest (>=3.2) + :pypi:`pytest-figleaf` py.test figleaf coverage plugin Jan 18, 2010 5 - Production/Stable N/A + :pypi:`pytest-file` Pytest File Mar 18, 2024 1 - Planning N/A + :pypi:`pytest-filecov` A pytest plugin to detect unused files Jun 27, 2021 4 - Beta pytest + :pypi:`pytest-filedata` easily load data from files Jan 17, 2019 4 - Beta N/A + :pypi:`pytest-filemarker` A pytest plugin that runs marked tests when files change. Dec 01, 2020 N/A pytest + :pypi:`pytest-file-watcher` Pytest-File-Watcher is a CLI tool that watches for changes in your code and runs pytest on the changed files. Mar 23, 2023 N/A pytest + :pypi:`pytest-filter-case` run test cases filter by mark Nov 05, 2020 N/A N/A + :pypi:`pytest-filter-subpackage` Pytest plugin for filtering based on sub-packages Mar 04, 2024 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-find-dependencies` A pytest plugin to find dependencies between tests Mar 16, 2024 4 - Beta pytest >=4.3.0 + :pypi:`pytest-finer-verdicts` A pytest plugin to treat non-assertion failures as test errors. Jun 18, 2020 N/A pytest (>=5.4.3) + :pypi:`pytest-firefox` pytest plugin to manipulate firefox Aug 08, 2017 3 - Alpha pytest (>=3.0.2) + :pypi:`pytest-fixture-classes` Fixtures as classes that work well with dependency injection, autocompletetion, type checkers, and language servers Sep 02, 2023 5 - Production/Stable pytest + :pypi:`pytest-fixturecollection` A pytest plugin to collect tests based on fixtures being used by tests Feb 22, 2024 4 - Beta pytest >=3.5.0 + :pypi:`pytest-fixture-config` Fixture configuration utils for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-fixture-maker` Pytest plugin to load fixtures from YAML files Sep 21, 2021 N/A N/A + :pypi:`pytest-fixture-marker` A pytest plugin to add markers based on fixtures used. Oct 11, 2020 5 - Production/Stable N/A + :pypi:`pytest-fixture-order` pytest plugin to control fixture evaluation order May 16, 2022 5 - Production/Stable pytest (>=3.0) + :pypi:`pytest-fixture-ref` Lets users reference fixtures without name matching magic. Nov 17, 2022 4 - Beta N/A + :pypi:`pytest-fixture-remover` A LibCST codemod to remove pytest fixtures applied via the usefixtures decorator, as well as its parametrizations. Feb 14, 2024 5 - Production/Stable N/A + :pypi:`pytest-fixture-rtttg` Warn or fail on fixture name clash Feb 23, 2022 N/A pytest (>=7.0.1,<8.0.0) + :pypi:`pytest-fixtures` Common fixtures for pytest May 01, 2019 5 - Production/Stable N/A + :pypi:`pytest-fixture-tools` Plugin for pytest which provides tools for fixtures Aug 18, 2020 6 - Mature pytest + :pypi:`pytest-fixture-typecheck` A pytest plugin to assert type annotations at runtime. Aug 24, 2021 N/A pytest + :pypi:`pytest-flake8` pytest plugin to check FLAKE8 requirements Mar 18, 2022 4 - Beta pytest (>=7.0) + :pypi:`pytest-flake8-path` A pytest fixture for testing flake8 plugins. Jul 10, 2023 5 - Production/Stable pytest + :pypi:`pytest-flake8-v2` pytest plugin to check FLAKE8 requirements Mar 01, 2022 5 - Production/Stable pytest (>=7.0) + :pypi:`pytest-flakefinder` Runs tests multiple times to expose flakiness. Oct 26, 2022 4 - Beta pytest (>=2.7.1) + :pypi:`pytest-flakes` pytest plugin to check source code with pyflakes Dec 02, 2021 5 - Production/Stable pytest (>=5) + :pypi:`pytest-flaptastic` Flaptastic py.test plugin Mar 17, 2019 N/A N/A + :pypi:`pytest-flask` A set of py.test fixtures to test Flask applications. Oct 23, 2023 5 - Production/Stable pytest >=5.2 + :pypi:`pytest-flask-ligand` Pytest fixtures and helper functions to use for testing flask-ligand microservices. Apr 25, 2023 4 - Beta pytest (~=7.3) + :pypi:`pytest-flask-sqlalchemy` A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 30, 2022 4 - Beta pytest (>=3.2.1) + :pypi:`pytest-flask-sqlalchemy-transactions` Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) + :pypi:`pytest-flexreport` Apr 15, 2023 4 - Beta pytest + :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jun 26, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-fluentbit` A pytest plugin in order to provide logs via fluentbit Jun 16, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-fly` pytest observer Apr 14, 2024 3 - Alpha pytest + :pypi:`pytest-flyte` Pytest fixtures for simplifying Flyte integration testing May 03, 2021 N/A pytest + :pypi:`pytest-focus` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest + :pypi:`pytest-forbid` Mar 07, 2023 N/A pytest (>=7.2.2,<8.0.0) + :pypi:`pytest-forcefail` py.test plugin to make the test failing regardless of pytest.mark.xfail May 15, 2018 4 - Beta N/A + :pypi:`pytest-forks` Fork helper for pytest Mar 05, 2024 N/A N/A + :pypi:`pytest-forward-compatability` A name to avoid typosquating pytest-foward-compatibility Sep 06, 2020 N/A N/A + :pypi:`pytest-forward-compatibility` A pytest plugin to shim pytest commandline options for fowards compatibility Sep 29, 2020 N/A N/A + :pypi:`pytest-frappe` Pytest Frappe Plugin - A set of pytest fixtures to test Frappe applications Oct 29, 2023 4 - Beta pytest>=7.0.0 + :pypi:`pytest-freezegun` Wrap tests with fixtures in freeze_time Jul 19, 2020 4 - Beta pytest (>=3.0.0) + :pypi:`pytest-freezer` Pytest plugin providing a fixture interface for spulec/freezegun Jun 21, 2023 N/A pytest >= 3.6 + :pypi:`pytest-freeze-reqs` Check if requirement files are frozen Apr 29, 2021 N/A N/A + :pypi:`pytest-frozen-uuids` Deterministically frozen UUID's for your tests Apr 17, 2022 N/A pytest (>=3.0) + :pypi:`pytest-func-cov` Pytest plugin for measuring function coverage Apr 15, 2021 3 - Alpha pytest (>=5) + :pypi:`pytest-funparam` An alternative way to parametrize test cases. Dec 02, 2021 4 - Beta pytest >=4.6.0 + :pypi:`pytest-fxa` pytest plugin for Firefox Accounts Aug 28, 2018 5 - Production/Stable N/A + :pypi:`pytest-fxtest` Oct 27, 2020 N/A N/A + :pypi:`pytest-fzf` fzf-based test selector for pytest Feb 07, 2024 4 - Beta pytest >=6.0.0 + :pypi:`pytest_gae` pytest plugin for apps written with Google's AppEngine Aug 03, 2016 3 - Alpha N/A + :pypi:`pytest-gather-fixtures` set up asynchronous pytest fixtures concurrently Apr 12, 2022 N/A pytest (>=6.0.0) + :pypi:`pytest-gc` The garbage collector plugin for py.test Feb 01, 2018 N/A N/A + :pypi:`pytest-gcov` Uses gcov to measure test coverage of a C library Feb 01, 2018 3 - Alpha N/A + :pypi:`pytest-gcs` GCS fixtures and fixture factories for Pytest. Mar 01, 2024 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-gee` The Python plugin for your GEE based packages. Feb 15, 2024 3 - Alpha pytest + :pypi:`pytest-gevent` Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest + :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) + :pypi:`pytest-gh-log-group` pytest plugin for gh actions Jan 11, 2022 3 - Alpha pytest + :pypi:`pytest-ghostinspector` For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A + :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Apr 12, 2024 N/A pytest>=3.6 + :pypi:`pytest-git` Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-gitconfig` Provide a gitconfig sandbox for testing Oct 15, 2023 4 - Beta pytest>=7.1.2 + :pypi:`pytest-gitcov` Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A + :pypi:`pytest-git-diff` Pytest plugin that allows the user to select the tests affected by a range of git commits Apr 02, 2024 N/A N/A + :pypi:`pytest-git-fixtures` Pytest fixtures for testing with git. Mar 11, 2021 4 - Beta pytest + :pypi:`pytest-github` Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A + :pypi:`pytest-github-actions-annotate-failures` pytest plugin to annotate failed tests with a workflow command for GitHub Actions May 04, 2023 5 - Production/Stable pytest (>=4.0.0) + :pypi:`pytest-github-report` Generate a GitHub report using pytest in GitHub Workflows Jun 03, 2022 4 - Beta N/A + :pypi:`pytest-gitignore` py.test plugin to ignore the same files as git Jul 17, 2015 4 - Beta N/A + :pypi:`pytest-gitlabci-parallelized` Parallelize pytest across GitLab CI workers. Mar 08, 2023 N/A N/A + :pypi:`pytest-gitlab-code-quality` Collects warnings while testing and generates a GitLab Code Quality Report. Apr 03, 2024 N/A pytest>=8.1.1 + :pypi:`pytest-gitlab-fold` Folds output sections in GitLab CI build log Dec 31, 2023 4 - Beta pytest >=2.6.0 + :pypi:`pytest-git-selector` Utility to select tests that have had its dependencies modified (as identified by git diff) Nov 17, 2022 N/A N/A + :pypi:`pytest-glamor-allure` Extends allure-pytest functionality Jul 22, 2022 4 - Beta pytest + :pypi:`pytest-gnupg-fixtures` Pytest fixtures for testing with gnupg. Mar 04, 2021 4 - Beta pytest + :pypi:`pytest-golden` Plugin for pytest that offloads expected outputs to data files Jul 18, 2022 N/A pytest (>=6.1.2) + :pypi:`pytest-goldie` A plugin to support golden tests with pytest. May 23, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-google-chat` Notify google chat channel for test results Mar 27, 2022 4 - Beta pytest + :pypi:`pytest-graphql-schema` Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A + :pypi:`pytest-greendots` Green progress dots Feb 08, 2014 3 - Alpha N/A + :pypi:`pytest-group-by-class` A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. Jun 27, 2023 5 - Production/Stable pytest (>=2.5) + :pypi:`pytest-growl` Growl notifications for pytest results. Jan 13, 2014 5 - Production/Stable N/A + :pypi:`pytest-grpc` pytest plugin for grpc May 01, 2020 N/A pytest (>=3.6.0) + :pypi:`pytest-grunnur` Py.Test plugin for Grunnur-based packages. Feb 05, 2023 N/A N/A + :pypi:`pytest_gui_status` Show pytest status in gui Jan 23, 2016 N/A pytest + :pypi:`pytest-hammertime` Display "🔨 " instead of "." for passed pytest tests. Jul 28, 2018 N/A pytest + :pypi:`pytest-hardware-test-report` A simple plugin to use with pytest Apr 01, 2024 4 - Beta pytest<9.0.0,>=8.0.0 + :pypi:`pytest-harmony` Chain tests and data with pytest Jan 17, 2023 N/A pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-harvest` Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes. Mar 16, 2024 5 - Production/Stable N/A + :pypi:`pytest-helm-chart` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Jun 15, 2020 4 - Beta pytest (>=5.4.2,<6.0.0) + :pypi:`pytest-helm-charts` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Feb 07, 2024 4 - Beta pytest (>=8.0.0,<9.0.0) + :pypi:`pytest-helm-templates` Pytest fixtures for unit testing the output of helm templates Apr 05, 2024 N/A pytest~=7.4.0; extra == "dev" + :pypi:`pytest-helper` Functions to help in using the pytest testing framework May 31, 2019 5 - Production/Stable N/A + :pypi:`pytest-helpers` pytest helpers May 17, 2020 N/A pytest + :pypi:`pytest-helpers-namespace` Pytest Helpers Namespace Plugin Dec 29, 2021 5 - Production/Stable pytest (>=6.0.0) + :pypi:`pytest-henry` Aug 29, 2023 N/A N/A + :pypi:`pytest-hidecaptured` Hide captured output May 04, 2018 4 - Beta pytest (>=2.8.5) + :pypi:`pytest-himark` A plugin that will filter pytest's test collection using a json file. It will read a json file provided with a --json argument in pytest command line (or in pytest.ini), search the markers key and automatically add -m option to the command line for filtering out the tests marked with disabled markers. Apr 14, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-historic` Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest + :pypi:`pytest-historic-hook` Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest + :pypi:`pytest-history` Pytest plugin to keep a history of your pytest runs Jan 14, 2024 N/A pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-home` Home directory fixtures Oct 09, 2023 5 - Production/Stable pytest + :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Apr 13, 2024 3 - Alpha pytest==8.1.1 + :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A + :pypi:`pytest-hot-reloading` Apr 18, 2024 N/A N/A + :pypi:`pytest-hot-test` A plugin that tracks test changes Dec 10, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-houdini` pytest plugin for testing code in Houdini. Feb 09, 2024 N/A pytest + :pypi:`pytest-hoverfly` Simplify working with Hoverfly from pytest Jan 30, 2023 N/A pytest (>=5.0) + :pypi:`pytest-hoverfly-wrapper` Integrates the Hoverfly HTTP proxy into Pytest Feb 27, 2023 5 - Production/Stable pytest (>=3.7.0) + :pypi:`pytest-hpfeeds` Helpers for testing hpfeeds in your python project Feb 28, 2023 4 - Beta pytest (>=6.2.4,<7.0.0) + :pypi:`pytest-html` pytest plugin for generating HTML reports Nov 07, 2023 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-html-cn` pytest plugin for generating HTML reports Aug 01, 2023 5 - Production/Stable N/A + :pypi:`pytest-html-lee` optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-html-merger` Pytest HTML reports merging utility Nov 11, 2023 N/A N/A + :pypi:`pytest-html-object-storage` Pytest report plugin for send HTML report on object-storage Jan 17, 2024 5 - Production/Stable N/A + :pypi:`pytest-html-profiling` Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) + :pypi:`pytest-html-reporter` Generates a static html report based on pytest framework Feb 13, 2022 N/A N/A + :pypi:`pytest-html-report-merger` Oct 23, 2023 N/A N/A + :pypi:`pytest-html-thread` pytest plugin for generating HTML reports Dec 29, 2020 5 - Production/Stable N/A + :pypi:`pytest-http` Fixture "http" for http requests Dec 05, 2019 N/A N/A + :pypi:`pytest-httpbin` Easily test your HTTP library against a local copy of httpbin May 08, 2023 5 - Production/Stable pytest ; extra == 'test' + :pypi:`pytest-httpdbg` A pytest plugin to record HTTP(S) requests with stack trace Jan 10, 2024 3 - Alpha pytest >=7.0.0 + :pypi:`pytest-http-mocker` Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A + :pypi:`pytest-httpretty` A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A + :pypi:`pytest_httpserver` pytest-httpserver is a httpserver for pytest Feb 24, 2024 3 - Alpha N/A + :pypi:`pytest-httptesting` http_testing framework on top of pytest Jul 24, 2023 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-httpx` Send responses to httpx. Feb 21, 2024 5 - Production/Stable pytest <9,>=7 + :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Feb 16, 2023 N/A pytest (>=7.2.1) + :pypi:`pytest-httpx-recorder` Recorder feature based on pytest_httpx, like recorder feature in responses. Jan 04, 2024 5 - Production/Stable pytest + :pypi:`pytest-hue` Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A + :pypi:`pytest-hylang` Pytest plugin to allow running tests written in hylang Mar 28, 2021 N/A pytest + :pypi:`pytest-hypo-25` help hypo module for pytest Jan 12, 2020 3 - Alpha N/A + :pypi:`pytest-iam` A fully functional OAUTH2 / OpenID Connect (OIDC) server to be used in your testsuite Apr 12, 2024 3 - Alpha pytest>=7.0.0 + :pypi:`pytest-ibutsu` A plugin to sent pytest results to an Ibutsu server Aug 05, 2022 4 - Beta pytest>=7.1 + :pypi:`pytest-icdiff` use icdiff for better error messages in pytest assertions Dec 05, 2023 4 - Beta pytest + :pypi:`pytest-idapro` A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A + :pypi:`pytest-idem` A pytest plugin to help with testing idem projects Dec 13, 2023 5 - Production/Stable N/A + :pypi:`pytest-idempotent` Pytest plugin for testing function idempotence. Jul 25, 2022 N/A N/A + :pypi:`pytest-ignore-flaky` ignore failures from flaky tests (pytest plugin) Apr 08, 2024 5 - Production/Stable pytest>=6.0 + :pypi:`pytest-ignore-test-results` A pytest plugin to ignore test results. Aug 17, 2023 2 - Pre-Alpha pytest>=7.0 + :pypi:`pytest-image-diff` Mar 09, 2023 3 - Alpha pytest + :pypi:`pytest-image-snapshot` A pytest plugin for image snapshot management and comparison. Dec 01, 2023 4 - Beta pytest >=3.5.0 + :pypi:`pytest-incremental` an incremental test runner (pytest plugin) Apr 24, 2021 5 - Production/Stable N/A + :pypi:`pytest-influxdb` Plugin for influxdb and pytest integration. Apr 20, 2021 N/A N/A + :pypi:`pytest-info-collector` pytest plugin to collect information from tests May 26, 2019 3 - Alpha N/A + :pypi:`pytest-info-plugin` Get executed interface information in pytest interface automation framework Sep 14, 2023 N/A N/A + :pypi:`pytest-informative-node` display more node ininformation. Apr 25, 2019 4 - Beta N/A + :pypi:`pytest-infrastructure` pytest stack validation prior to testing executing Apr 12, 2020 4 - Beta N/A + :pypi:`pytest-ini` Reuse pytest.ini to store env variables Apr 26, 2022 N/A N/A + :pypi:`pytest-initry` Plugin for sending automation test data from Pytest to the initry Apr 14, 2024 N/A pytest<9.0.0,>=8.1.1 + :pypi:`pytest-inline` A pytest plugin for writing inline tests. Oct 19, 2023 4 - Beta pytest >=7.0.0 + :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Dec 13, 2023 5 - Production/Stable pytest + :pypi:`pytest-inmanta-extensions` Inmanta tests package Apr 02, 2024 5 - Production/Stable N/A + :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules Apr 15, 2024 5 - Production/Stable N/A + :pypi:`pytest-inmanta-yang` Common fixtures used in inmanta yang related modules Feb 22, 2024 4 - Beta pytest + :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A + :pypi:`pytest-in-robotframework` The extension enables easy execution of pytest tests within the Robot Framework environment. Mar 02, 2024 N/A pytest + :pypi:`pytest-insper` Pytest plugin for courses at Insper Mar 21, 2024 N/A pytest + :pypi:`pytest-insta` A practical snapshot testing plugin for pytest Feb 19, 2024 N/A pytest (>=7.2.0,<9.0.0) + :pypi:`pytest-instafail` pytest plugin to show failures instantly Mar 31, 2023 4 - Beta pytest (>=5) + :pypi:`pytest-instrument` pytest plugin to instrument tests Apr 05, 2020 5 - Production/Stable pytest (>=5.1.0) + :pypi:`pytest-integration` Organizing pytests by integration or not Nov 17, 2022 N/A N/A + :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest May 22, 2023 N/A pytest (>=5.2) + :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A + :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) + :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. Feb 09, 2024 4 - Beta pytest + :pypi:`pytest-invenio` Pytest fixtures for Invenio. Feb 28, 2024 5 - Production/Stable pytest <7.2.0,>=6 + :pypi:`pytest-involve` Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Mar 20, 2013 2 - Pre-Alpha N/A + :pypi:`pytest-ipynb` THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A + :pypi:`pytest-ipywidgets` Apr 08, 2024 N/A pytest + :pypi:`pytest-isolate` Feb 20, 2023 4 - Beta pytest + :pypi:`pytest-isort` py.test plugin to check import ordering using isort Mar 05, 2024 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-it` Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 29, 2024 4 - Beta N/A + :pypi:`pytest-iterassert` Nicer list and iterable assertion messages for pytest May 11, 2020 3 - Alpha N/A + :pypi:`pytest-iters` A contextmanager pytest fixture for handling multiple mock iters May 24, 2022 N/A N/A + :pypi:`pytest_jar_yuan` A allure and pytest used package Dec 12, 2022 N/A N/A + :pypi:`pytest-jasmine` Run jasmine tests from your pytest test suite Nov 04, 2017 1 - Planning N/A + :pypi:`pytest-jelastic` Pytest plugin defining the necessary command-line options to pass to pytests testing a Jelastic environment. Nov 16, 2022 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-jest` A custom jest-pytest oriented Pytest reporter May 22, 2018 4 - Beta pytest (>=3.3.2) + :pypi:`pytest-jinja` A plugin to generate customizable jinja-based HTML reports in pytest Oct 04, 2022 3 - Alpha pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-jira` py.test JIRA integration plugin, using markers Apr 12, 2024 3 - Alpha N/A + :pypi:`pytest-jira-xfail` Plugin skips (xfail) tests if unresolved Jira issue(s) linked Jun 19, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-jira-xray` pytest plugin to integrate tests with JIRA XRAY Mar 27, 2024 4 - Beta pytest>=6.2.4 + :pypi:`pytest-job-selection` A pytest plugin for load balancing test suites Jan 30, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-jobserver` Limit parallel tests with posix jobserver. May 15, 2019 5 - Production/Stable pytest + :pypi:`pytest-joke` Test failures are better served with humor. Oct 08, 2019 4 - Beta pytest (>=4.2.1) + :pypi:`pytest-json` Generate JSON test reports Jan 18, 2016 4 - Beta N/A + :pypi:`pytest-json-fixtures` JSON output for the --fixtures flag Mar 14, 2023 4 - Beta N/A + :pypi:`pytest-jsonlint` UNKNOWN Aug 04, 2016 N/A N/A + :pypi:`pytest-json-report` A pytest plugin to report test results as JSON files Mar 15, 2022 4 - Beta pytest (>=3.8.0) + :pypi:`pytest-json-report-wip` A pytest plugin to report test results as JSON files Oct 28, 2023 4 - Beta pytest >=3.8.0 + :pypi:`pytest-jsonschema` A pytest plugin to perform JSONSchema validations Mar 27, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-jtr` pytest plugin supporting json test report output Apr 15, 2024 N/A pytest<8.0.0,>=7.1.2 + :pypi:`pytest-jupyter` A pytest plugin for testing Jupyter libraries and extensions. Apr 04, 2024 4 - Beta pytest>=7.0 + :pypi:`pytest-jupyterhub` A reusable JupyterHub pytest plugin Apr 25, 2023 5 - Production/Stable pytest + :pypi:`pytest-kafka` Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest Jun 14, 2023 N/A pytest + :pypi:`pytest-kafkavents` A plugin to send pytest events to Kafka Sep 08, 2021 4 - Beta pytest + :pypi:`pytest-kasima` Display horizontal lines above and below the captured standard output for easy viewing. Jan 26, 2023 5 - Production/Stable pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-keep-together` Pytest plugin to customize test ordering by running all 'related' tests together Dec 07, 2022 5 - Production/Stable pytest + :pypi:`pytest-kexi` Apr 29, 2022 N/A pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-keyring` A Pytest plugin to access the system's keyring to provide credentials for tests Oct 01, 2023 N/A pytest (>=7.1) + :pypi:`pytest-kind` Kubernetes test support with KIND for pytest Nov 30, 2022 5 - Production/Stable N/A + :pypi:`pytest-kivy` Kivy GUI tests fixtures using pytest Jul 06, 2021 4 - Beta pytest (>=3.6) + :pypi:`pytest-knows` A pytest plugin that can automaticly skip test case based on dependence info calculated by trace Aug 22, 2014 N/A N/A + :pypi:`pytest-konira` Run Konira DSL tests with py.test Oct 09, 2011 N/A N/A + :pypi:`pytest-koopmans` A plugin for testing the koopmans package Nov 21, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-krtech-common` pytest krtech common library Nov 28, 2016 4 - Beta N/A + :pypi:`pytest-kubernetes` Sep 14, 2023 N/A pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-kuunda` pytest plugin to help with test data setup for PySpark tests Feb 25, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-kwparametrize` Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks Jan 22, 2021 N/A pytest (>=6) + :pypi:`pytest-lambda` Define pytest fixtures with lambda functions. Aug 20, 2022 3 - Alpha pytest (>=3.6,<8) + :pypi:`pytest-lamp` Jan 06, 2017 3 - Alpha N/A + :pypi:`pytest-langchain` Pytest-style test runner for langchain agents Feb 26, 2023 N/A pytest + :pypi:`pytest-lark` Create fancy and clear HTML test reports. Nov 05, 2023 N/A N/A + :pypi:`pytest-launchable` Launchable Pytest Plugin Apr 05, 2023 N/A pytest (>=4.2.0) + :pypi:`pytest-layab` Pytest fixtures for layab. Oct 05, 2020 5 - Production/Stable N/A + :pypi:`pytest-lazy-fixture` It helps to use fixtures in pytest.mark.parametrize Feb 01, 2020 4 - Beta pytest (>=3.2.5) + :pypi:`pytest-lazy-fixtures` Allows you to use fixtures in @pytest.mark.parametrize. Mar 16, 2024 N/A pytest (>=7) + :pypi:`pytest-ldap` python-ldap fixtures for pytest Aug 18, 2020 N/A pytest + :pypi:`pytest-leak-finder` Find the test that's leaking before the one that fails Feb 15, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-leaks` A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A + :pypi:`pytest-leaping` A simple plugin to use with pytest Mar 27, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-level` Select tests of a given level or lower Oct 21, 2019 N/A pytest + :pypi:`pytest-libfaketime` A python-libfaketime plugin for pytest Apr 12, 2024 4 - Beta pytest>=3.0.0 + :pypi:`pytest-libiio` A pytest plugin to manage interfacing with libiio contexts Dec 22, 2023 4 - Beta N/A + :pypi:`pytest-libnotify` Pytest plugin that shows notifications about the test run Apr 02, 2021 3 - Alpha pytest + :pypi:`pytest-ligo` Jan 16, 2020 4 - Beta N/A + :pypi:`pytest-lineno` A pytest plugin to show the line numbers of test functions Dec 04, 2020 N/A pytest + :pypi:`pytest-line-profiler` Profile code executed by pytest Aug 10, 2023 4 - Beta pytest >=3.5.0 + :pypi:`pytest-line-profiler-apn` Profile code executed by pytest Dec 05, 2022 N/A pytest (>=3.5.0) + :pypi:`pytest-lisa` Pytest plugin for organizing tests. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) + :pypi:`pytest-listener` A simple network listener May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-litf` A pytest plugin that stream output in LITF format Jan 18, 2021 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-litter` Pytest plugin which verifies that tests do not modify file trees. Nov 23, 2023 4 - Beta pytest >=6.1 + :pypi:`pytest-live` Live results for pytest Mar 08, 2020 N/A pytest + :pypi:`pytest-local-badge` Generate local badges (shields) reporting your test suite status. Jan 15, 2023 N/A pytest (>=6.1.0) + :pypi:`pytest-localftpserver` A PyTest plugin which provides an FTP fixture for your tests Oct 14, 2023 5 - Production/Stable pytest + :pypi:`pytest-localserver` pytest plugin to test server connections locally. Oct 12, 2023 4 - Beta N/A + :pypi:`pytest-localstack` Pytest plugin for AWS integration tests Jun 07, 2023 4 - Beta pytest (>=6.0.0,<7.0.0) + :pypi:`pytest-lock` pytest-lock is a pytest plugin that allows you to "lock" the results of unit tests, storing them in a local cache. This is particularly useful for tests that are resource-intensive or don't need to be run every time. When the tests are run subsequently, pytest-lock will compare the current results with the locked results and issue a warning if there are any discrepancies. Feb 03, 2024 N/A pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-lockable` lockable resource plugin for pytest Jan 24, 2024 5 - Production/Stable pytest + :pypi:`pytest-locker` Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Oct 29, 2021 N/A pytest (>=5.4) + :pypi:`pytest-log` print log Aug 15, 2021 N/A pytest (>=3.8) + :pypi:`pytest-logbook` py.test plugin to capture logbook log messages Nov 23, 2015 5 - Production/Stable pytest (>=2.8) + :pypi:`pytest-logdog` Pytest plugin to test logging Jun 15, 2021 1 - Planning pytest (>=6.2.0) + :pypi:`pytest-logfest` Pytest plugin providing three logger fixtures with basic or full writing to log files Jul 21, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-logger` Plugin configuring handlers for loggers from Python logging module. Mar 10, 2024 5 - Production/Stable pytest (>=3.2) + :pypi:`pytest-logging` Configures logging and allows tweaking the log level with a py.test flag Nov 04, 2015 4 - Beta N/A + :pypi:`pytest-logging-end-to-end-test-tool` Sep 23, 2022 N/A pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-logikal` Common testing environment Mar 30, 2024 5 - Production/Stable pytest==8.1.1 + :pypi:`pytest-log-report` Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A + :pypi:`pytest-loguru` Pytest Loguru Mar 20, 2024 5 - Production/Stable pytest; extra == "test" + :pypi:`pytest-loop` pytest plugin for looping tests Mar 30, 2024 5 - Production/Stable pytest + :pypi:`pytest-lsp` A pytest plugin for end-to-end testing of language servers Feb 07, 2024 3 - Alpha pytest + :pypi:`pytest-manual-marker` pytest marker for marking manual tests Aug 04, 2022 3 - Alpha pytest>=7 + :pypi:`pytest-markdoctest` A pytest plugin to doctest your markdown files Jul 22, 2022 4 - Beta pytest (>=6) + :pypi:`pytest-markdown` Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) + :pypi:`pytest-markdown-docs` Run markdown code fences through pytest Mar 05, 2024 N/A pytest (>=7.0.0) + :pypi:`pytest-marker-bugzilla` py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A + :pypi:`pytest-markers-presence` A simple plugin to detect missed pytest tags and markers" Feb 04, 2021 4 - Beta pytest (>=6.0) + :pypi:`pytest-markfiltration` UNKNOWN Nov 08, 2011 3 - Alpha N/A + :pypi:`pytest-mark-no-py3` pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest + :pypi:`pytest-marks` UNKNOWN Nov 23, 2012 3 - Alpha N/A + :pypi:`pytest-matcher` Easy way to match captured \`pytest\` output against expectations stored in files Mar 15, 2024 5 - Production/Stable pytest + :pypi:`pytest-match-skip` Skip matching marks. Matches partial marks using wildcards. May 15, 2019 4 - Beta pytest (>=4.4.1) + :pypi:`pytest-mat-report` this is report Jan 20, 2021 N/A N/A + :pypi:`pytest-matrix` Provide tools for generating tests from combinations of fixtures. Jun 24, 2020 5 - Production/Stable pytest (>=5.4.3,<6.0.0) + :pypi:`pytest-maxcov` Compute the maximum coverage available through pytest with the minimum execution time cost Sep 24, 2023 N/A pytest (>=7.4.0,<8.0.0) + :pypi:`pytest-maybe-context` Simplify tests with warning and exception cases. Apr 16, 2023 N/A pytest (>=7,<8) + :pypi:`pytest-maybe-raises` Pytest fixture for optional exception testing. May 27, 2022 N/A pytest ; extra == 'dev' + :pypi:`pytest-mccabe` pytest plugin to run the mccabe code complexity checker. Jul 22, 2020 3 - Alpha pytest (>=5.4.0) + :pypi:`pytest-md` Plugin for generating Markdown reports for pytest results Jul 11, 2019 3 - Alpha pytest (>=4.2.1) + :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. Feb 04, 2024 4 - Beta pytest !=6.0.0,<9,>=3.3.2 + :pypi:`pytest-meilisearch` Pytest helpers for testing projects using Meilisearch Feb 15, 2024 N/A pytest (>=7.4.3) + :pypi:`pytest-memlog` Log memory usage during tests May 03, 2023 N/A pytest (>=7.3.0,<8.0.0) + :pypi:`pytest-memprof` Estimates memory consumption of test functions Mar 29, 2019 4 - Beta N/A + :pypi:`pytest-memray` A simple plugin to use with pytest Apr 18, 2024 N/A pytest>=7.2 + :pypi:`pytest-menu` A pytest plugin for console based interactive test selection just after the collection phase Oct 04, 2017 3 - Alpha pytest (>=2.4.2) + :pypi:`pytest-mercurial` pytest plugin to write integration tests for projects using Mercurial Python internals Nov 21, 2020 1 - Planning N/A + :pypi:`pytest-mesh` pytest_mesh插件 Aug 05, 2022 N/A pytest (==7.1.2) + :pypi:`pytest-message` Pytest plugin for sending report message of marked tests execution Aug 04, 2022 N/A pytest (>=6.2.5) + :pypi:`pytest-messenger` Pytest to Slack reporting plugin Nov 24, 2022 5 - Production/Stable N/A + :pypi:`pytest-metadata` pytest plugin for test session metadata Feb 12, 2024 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-metrics` Custom metrics report for pytest Apr 04, 2020 N/A pytest + :pypi:`pytest-mh` Pytest multihost plugin Mar 14, 2024 N/A pytest + :pypi:`pytest-mimesis` Mimesis integration with the pytest test runner Mar 21, 2020 5 - Production/Stable pytest (>=4.2) + :pypi:`pytest-minecraft` A pytest plugin for running tests against Minecraft releases Apr 06, 2022 N/A pytest (>=6.0.1) + :pypi:`pytest-mini` A plugin to test mp Feb 06, 2023 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-minio-mock` A pytest plugin for mocking Minio S3 interactions Apr 15, 2024 N/A pytest>=5.0.0 + :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests Mar 07, 2024 N/A pytest >=7.0 + :pypi:`pytest-ml` Test your machine learning! May 04, 2019 4 - Beta N/A + :pypi:`pytest-mocha` pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) + :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest Mar 21, 2024 5 - Production/Stable pytest>=6.2.5 + :pypi:`pytest-mock-api` A mock API server with configurable routes and responses available as a fixture. Feb 13, 2019 1 - Planning pytest (>=4.0.0) + :pypi:`pytest-mock-generator` A pytest fixture wrapper for https://pypi.org/project/mock-generator May 16, 2022 5 - Production/Stable N/A + :pypi:`pytest-mock-helper` Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest + :pypi:`pytest-mockito` Base fixtures for mockito Jul 11, 2018 4 - Beta N/A + :pypi:`pytest-mockredis` An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A + :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Apr 11, 2024 N/A pytest>=1.0 + :pypi:`pytest-mock-server` Mock server plugin for pytest Jan 09, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) + :pypi:`pytest-mocktcp` A pytest plugin for testing TCP clients Oct 11, 2022 N/A pytest + :pypi:`pytest-modalt` Massively distributed pytest runs using modal.com Feb 27, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-modified-env` Pytest plugin to fail a test if it leaves modified \`os.environ\` afterwards. Jan 29, 2022 4 - Beta N/A + :pypi:`pytest-modifyjunit` Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A + :pypi:`pytest-modifyscope` pytest plugin to modify fixture scope Apr 12, 2020 N/A pytest + :pypi:`pytest-molecule` PyTest Molecule Plugin :: discover and run molecule tests Mar 29, 2022 5 - Production/Stable pytest (>=7.0.0) + :pypi:`pytest-molecule-JC` PyTest Molecule Plugin :: discover and run molecule tests Jul 18, 2023 5 - Production/Stable pytest (>=7.0.0) + :pypi:`pytest-mongo` MongoDB process and client fixtures plugin for Pytest. Mar 13, 2024 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-mongodb` pytest plugin for MongoDB fixtures May 16, 2023 5 - Production/Stable N/A + :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Jun 25, 2023 5 - Production/Stable pytest + :pypi:`pytest-monkeyplus` pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A + :pypi:`pytest-monkeytype` pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A + :pypi:`pytest-moto` Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A + :pypi:`pytest-motor` A pytest plugin for motor, the non-blocking MongoDB driver. Jul 21, 2021 3 - Alpha pytest + :pypi:`pytest-mp` A test batcher for multiprocessed Pytest runs May 23, 2018 4 - Beta pytest + :pypi:`pytest-mpi` pytest plugin to collect information from tests Jan 08, 2022 3 - Alpha pytest + :pypi:`pytest-mpiexec` pytest plugin for running individual tests with mpiexec Apr 13, 2023 3 - Alpha pytest + :pypi:`pytest-mpl` pytest plugin to help with testing figures output from Matplotlib Feb 14, 2024 4 - Beta pytest + :pypi:`pytest-mproc` low-startup-overhead, scalable, distributed-testing pytest plugin Nov 15, 2022 4 - Beta pytest (>=6) + :pypi:`pytest-mqtt` pytest-mqtt supports testing systems based on MQTT Mar 31, 2024 4 - Beta pytest<8; extra == "test" + :pypi:`pytest-multihost` Utility for writing multi-host tests for pytest Apr 07, 2020 4 - Beta N/A + :pypi:`pytest-multilog` Multi-process logs handling and other helpers for pytest Jan 17, 2023 N/A pytest + :pypi:`pytest-multithreading` a pytest plugin for th and concurrent testing Dec 07, 2022 N/A N/A + :pypi:`pytest-multithreading-allure` pytest_multithreading_allure Nov 25, 2022 N/A N/A + :pypi:`pytest-mutagen` Add the mutation testing feature to pytest Jul 24, 2020 N/A pytest (>=5.4) + :pypi:`pytest-my-cool-lib` Nov 02, 2023 N/A pytest (>=7.1.3,<8.0.0) + :pypi:`pytest-mypy` Mypy static type checker plugin for Pytest Dec 18, 2022 4 - Beta pytest (>=6.2) ; python_version >= "3.10" + :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" + :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Mar 31, 2024 4 - Beta pytest>=7.0.0 + :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A pytest>=6.0.0 + :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Mar 04, 2024 N/A pytest>=7,<9 + :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Oct 30, 2023 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-ndb` pytest notebook debugger Oct 15, 2023 N/A pytest + :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) + :pypi:`pytest-neo` pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Jan 08, 2022 3 - Alpha pytest (>=6.2.0) + :pypi:`pytest-neos` Pytest plugin for neos Apr 15, 2024 1 - Planning N/A + :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Mar 07, 2024 N/A pytest <7.3,>=3.5.0 + :pypi:`pytest-network` A simple plugin to disable network on socket level. May 07, 2020 N/A N/A + :pypi:`pytest-network-endpoints` Network endpoints plugin for pytest Mar 06, 2022 N/A pytest + :pypi:`pytest-never-sleep` pytest plugin helps to avoid adding tests without mock \`time.sleep\` May 05, 2021 3 - Alpha pytest (>=3.5.1) + :pypi:`pytest-nginx` nginx fixture for pytest Aug 12, 2017 5 - Production/Stable N/A + :pypi:`pytest-nginx-iplweb` nginx fixture for pytest - iplweb temporary fork Mar 01, 2019 5 - Production/Stable N/A + :pypi:`pytest-ngrok` Jan 20, 2022 3 - Alpha pytest + :pypi:`pytest-ngsfixtures` pytest ngs fixtures Sep 06, 2019 2 - Pre-Alpha pytest (>=5.0.0) + :pypi:`pytest-nhsd-apim` Pytest plugin accessing NHSDigital's APIM proxies Feb 16, 2024 N/A pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-nice` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest + :pypi:`pytest-nice-parametrize` A small snippet for nicer PyTest's Parametrize Apr 17, 2021 5 - Production/Stable N/A + :pypi:`pytest_nlcov` Pytest plugin to get the coverage of the new lines (based on git diff) only Apr 11, 2024 N/A N/A + :pypi:`pytest-nocustom` Run all tests without custom markers Apr 11, 2024 5 - Production/Stable N/A + :pypi:`pytest-node-dependency` pytest plugin for controlling execution flow Apr 10, 2024 5 - Production/Stable N/A + :pypi:`pytest-nodev` Test-driven source code search for Python. Jul 21, 2016 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-nogarbage` Ensure a test produces no garbage Aug 29, 2021 5 - Production/Stable pytest (>=4.6.0) + :pypi:`pytest-nose-attrib` pytest plugin to use nose @attrib marks decorators and pick tests based on attributes and partially uses nose-attrib plugin approach Aug 13, 2023 N/A N/A + :pypi:`pytest_notebook` A pytest plugin for testing Jupyter Notebooks. Nov 28, 2023 4 - Beta pytest>=3.5.0 + :pypi:`pytest-notice` Send pytest execution result email Nov 05, 2020 N/A N/A + :pypi:`pytest-notification` A pytest plugin for sending a desktop notification and playing a sound upon completion of tests Jun 19, 2020 N/A pytest (>=4) + :pypi:`pytest-notifier` A pytest plugin to notify test result Jun 12, 2020 3 - Alpha pytest + :pypi:`pytest_notify` Get notifications when your tests ends Jul 05, 2017 N/A pytest>=3.0.0 + :pypi:`pytest-notimplemented` Pytest markers for not implemented features and tests. Aug 27, 2019 N/A pytest (>=5.1,<6.0) + :pypi:`pytest-notion` A PyTest Reporter to send test runs to Notion.so Aug 07, 2019 N/A N/A + :pypi:`pytest-nunit` A pytest plugin for generating NUnit3 test result XML output Feb 26, 2024 5 - Production/Stable N/A + :pypi:`pytest-oar` PyTest plugin for the OAR testing framework May 02, 2023 N/A pytest>=6.0.1 + :pypi:`pytest-object-getter` Import any object from a 3rd party module while mocking its namespace on demand. Jul 31, 2022 5 - Production/Stable pytest + :pypi:`pytest-ochrus` pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A + :pypi:`pytest-odc` A pytest plugin for simplifying ODC database tests Aug 04, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-odoo` py.test plugin to run Odoo tests Jul 06, 2023 4 - Beta pytest (>=7.2.0) + :pypi:`pytest-odoo-fixtures` Project description Jun 25, 2019 N/A N/A + :pypi:`pytest-oerp` pytest plugin to test OpenERP modules Feb 28, 2012 3 - Alpha N/A + :pypi:`pytest-offline` Mar 09, 2023 1 - Planning pytest (>=7.0.0,<8.0.0) + :pypi:`pytest-ogsm-plugin` 针对特定项目定制化插件,优化了pytest报告展示方式,并添加了项目所需特定参数 May 16, 2023 N/A N/A + :pypi:`pytest-ok` The ultimate pytest output plugin Apr 01, 2019 4 - Beta N/A + :pypi:`pytest-only` Use @pytest.mark.only to run a single test Mar 09, 2024 5 - Production/Stable pytest (<7.1) ; python_full_version <= "3.6.0" + :pypi:`pytest-oof` A Pytest plugin providing structured, programmatic access to a test run's results Dec 11, 2023 4 - Beta N/A + :pypi:`pytest-oot` Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A + :pypi:`pytest-openfiles` Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) + :pypi:`pytest-opentelemetry` A pytest plugin for instrumenting test runs via OpenTelemetry Oct 01, 2023 N/A pytest + :pypi:`pytest-opentmi` pytest plugin for publish results to opentmi Jun 02, 2022 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-operator` Fixtures for Operators Sep 28, 2022 N/A pytest + :pypi:`pytest-optional` include/exclude values of fixtures in pytest Oct 07, 2015 N/A N/A + :pypi:`pytest-optional-tests` Easy declaration of optional tests (i.e., that are not run by default) Jul 09, 2019 4 - Beta pytest (>=4.5.0) + :pypi:`pytest-orchestration` A pytest plugin for orchestrating tests Jul 18, 2019 N/A N/A + :pypi:`pytest-order` pytest plugin to run your tests in a specific order Apr 02, 2024 4 - Beta pytest>=5.0; python_version < "3.10" + :pypi:`pytest-ordering` pytest plugin to run your tests in a specific order Nov 14, 2018 4 - Beta pytest + :pypi:`pytest-order-modify` 新增run_marker 来自定义用例的执行顺序 Nov 04, 2022 N/A N/A + :pypi:`pytest-osxnotify` OS X notifications for py.test results. May 15, 2015 N/A N/A + :pypi:`pytest-ot` A pytest plugin for instrumenting test runs via OpenTelemetry Mar 21, 2024 N/A pytest; extra == "dev" + :pypi:`pytest-otel` OpenTelemetry plugin for Pytest Mar 18, 2024 N/A pytest==8.1.1 + :pypi:`pytest-override-env-var` Pytest mark to override a value of an environment variable. Feb 25, 2023 N/A N/A + :pypi:`pytest-owner` Add owner mark for tests Apr 25, 2022 N/A N/A + :pypi:`pytest-pact` A simple plugin to use with pytest Jan 07, 2019 4 - Beta N/A + :pypi:`pytest-pahrametahrize` Parametrize your tests with a Boston accent. Nov 24, 2021 4 - Beta pytest (>=6.0,<7.0) + :pypi:`pytest-parallel` a pytest plugin for parallel and concurrent testing Oct 10, 2021 3 - Alpha pytest (>=3.0.0) + :pypi:`pytest-parallel-39` a pytest plugin for parallel and concurrent testing Jul 12, 2021 3 - Alpha pytest (>=3.0.0) + :pypi:`pytest-parallelize-tests` pytest plugin that parallelizes test execution across multiple hosts Jan 27, 2023 4 - Beta N/A + :pypi:`pytest-param` pytest plugin to test all, first, last or random params Sep 11, 2016 4 - Beta pytest (>=2.6.0) + :pypi:`pytest-paramark` Configure pytest fixtures using a combination of"parametrize" and markers Jan 10, 2020 4 - Beta pytest (>=4.5.0) + :pypi:`pytest-parameterize-from-files` A pytest plugin that parameterizes tests from data files. Feb 15, 2024 4 - Beta pytest>=7.2.0 + :pypi:`pytest-parametrization` Simpler PyTest parametrization May 22, 2022 5 - Production/Stable N/A + :pypi:`pytest-parametrize-cases` A more user-friendly way to write parametrized tests. Mar 13, 2022 N/A pytest (>=6.1.2) + :pypi:`pytest-parametrized` Pytest decorator for parametrizing tests with default iterables. Nov 03, 2023 5 - Production/Stable pytest + :pypi:`pytest-parametrize-suite` A simple pytest extension for creating a named test suite. Jan 19, 2023 5 - Production/Stable pytest + :pypi:`pytest_param_files` Create pytest parametrize decorators from external files. Jul 29, 2023 N/A pytest + :pypi:`pytest-param-scope` pytest parametrize scope fixture workaround Oct 18, 2023 N/A pytest + :pypi:`pytest-parawtf` Finally spell paramete?ri[sz]e correctly Dec 03, 2018 4 - Beta pytest (>=3.6.0) + :pypi:`pytest-pass` Check out https://github.com/elilutsky/pytest-pass Dec 04, 2019 N/A N/A + :pypi:`pytest-passrunner` Pytest plugin providing the 'run_on_pass' marker Feb 10, 2021 5 - Production/Stable pytest (>=4.6.0) + :pypi:`pytest-paste-config` Allow setting the path to a paste config file Sep 18, 2013 3 - Alpha N/A + :pypi:`pytest-patch` An automagic \`patch\` fixture that can patch objects directly or by name. Apr 29, 2023 3 - Alpha pytest (>=7.0.0) + :pypi:`pytest-patches` A contextmanager pytest fixture for handling multiple mock patches Aug 30, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-patterns` pytest plugin to make testing complicated long string output easy to write and easy to debug Nov 17, 2023 4 - Beta N/A + :pypi:`pytest-pdb` pytest plugin which adds pdb helper commands related to pytest. Jul 31, 2018 N/A N/A + :pypi:`pytest-peach` pytest plugin for fuzzing with Peach API Security Apr 12, 2019 4 - Beta pytest (>=2.8.7) + :pypi:`pytest-pep257` py.test plugin for pep257 Jul 09, 2016 N/A N/A + :pypi:`pytest-pep8` pytest plugin to check PEP8 requirements Apr 27, 2014 N/A N/A + :pypi:`pytest-percent` Change the exit code of pytest test sessions when a required percent of tests pass. May 21, 2020 N/A pytest (>=5.2.0) + :pypi:`pytest-percents` Mar 16, 2024 N/A N/A + :pypi:`pytest-perf` Run performance tests against the mainline code. Jan 28, 2024 5 - Production/Stable pytest >=6 ; extra == 'testing' + :pypi:`pytest-performance` A simple plugin to ensure the execution of critical sections of code has not been impacted Sep 11, 2020 5 - Production/Stable pytest (>=3.7.0) + :pypi:`pytest-performancetotal` A performance plugin for pytest Mar 19, 2024 4 - Beta N/A + :pypi:`pytest-persistence` Pytest tool for persistent objects Jul 04, 2023 N/A N/A + :pypi:`pytest-pexpect` Pytest pexpect plugin. Mar 27, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-pg` A tiny plugin for pytest which runs PostgreSQL in Docker Apr 03, 2024 5 - Production/Stable pytest>=6.0.0 + :pypi:`pytest-pgsql` Pytest plugins and helpers for tests using a Postgres database. May 13, 2020 5 - Production/Stable pytest (>=3.0.0) + :pypi:`pytest-phmdoctest` pytest plugin to test Python examples in Markdown using phmdoctest. Apr 15, 2022 4 - Beta pytest (>=5.4.3) + :pypi:`pytest-picked` Run the tests related to the changed files Jul 27, 2023 N/A pytest (>=3.7.0) + :pypi:`pytest-pigeonhole` Jun 25, 2018 5 - Production/Stable pytest (>=3.4) + :pypi:`pytest-pikachu` Show surprise when tests are passing Aug 05, 2021 5 - Production/Stable pytest + :pypi:`pytest-pilot` Slice in your test base thanks to powerful markers. Oct 09, 2020 5 - Production/Stable N/A + :pypi:`pytest-pingguo-pytest-plugin` pingguo test Oct 26, 2022 4 - Beta N/A + :pypi:`pytest-pings` 🦊 The pytest plugin for Firefox Telemetry 📊 Jun 29, 2019 3 - Alpha pytest (>=5.0.0) + :pypi:`pytest-pinned` A simple pytest plugin for pinning tests Sep 17, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-pinpoint` A pytest plugin which runs SBFL algorithms to detect faults. Sep 25, 2020 N/A pytest (>=4.4.0) + :pypi:`pytest-pipeline` Pytest plugin for functional testing of data analysispipelines Jan 24, 2017 3 - Alpha N/A + :pypi:`pytest-pitch` runs tests in an order such that coverage increases as fast as possible Nov 02, 2023 4 - Beta pytest >=7.3.1 + :pypi:`pytest-platform-markers` Markers for pytest to skip tests on specific platforms Sep 09, 2019 4 - Beta pytest (>=3.6.0) + :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A + :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) + :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Feb 02, 2024 N/A pytest (<9.0.0,>=6.2.4) + :pypi:`pytest_playwright_async` ASYNC Pytest plugin for Playwright Feb 25, 2024 N/A N/A + :pypi:`pytest-playwright-asyncio` Aug 29, 2023 N/A N/A + :pypi:`pytest-playwright-enhanced` A pytest plugin for playwright python Mar 24, 2024 N/A pytest<9.0.0,>=8.0.0 + :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A + :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A + :pypi:`pytest-playwright-visual` A pytest fixture for visual testing with Playwright Apr 28, 2022 N/A N/A + :pypi:`pytest-plone` Pytest plugin to test Plone addons Jan 05, 2023 3 - Alpha pytest + :pypi:`pytest-plt` Fixtures for quickly making Matplotlib plots in tests Jan 17, 2024 5 - Production/Stable pytest + :pypi:`pytest-plugin-helpers` A plugin to help developing and testing other plugins Nov 23, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-plus` PyTest Plus Plugin :: extends pytest functionality Mar 26, 2024 5 - Production/Stable pytest>=7.4.2 + :pypi:`pytest-pmisc` Mar 21, 2019 5 - Production/Stable N/A + :pypi:`pytest-pogo` Pytest plugin for pogo-migrate Mar 11, 2024 1 - Planning pytest (>=7,<9) + :pypi:`pytest-pointers` Pytest plugin to define functions you test with special marks for better navigation and reports Dec 26, 2022 N/A N/A + :pypi:`pytest-pokie` Pokie plugin for pytest Oct 19, 2023 5 - Production/Stable N/A + :pypi:`pytest-polarion-cfme` pytest plugin for collecting test cases and recording test results Nov 13, 2017 3 - Alpha N/A + :pypi:`pytest-polarion-collect` pytest plugin for collecting polarion test cases data Jun 18, 2020 3 - Alpha pytest + :pypi:`pytest-polecat` Provides Polecat pytest fixtures Aug 12, 2019 4 - Beta N/A + :pypi:`pytest-ponyorm` PonyORM in Pytest Oct 31, 2018 N/A pytest (>=3.1.1) + :pypi:`pytest-poo` Visualize your crappy tests Mar 25, 2021 5 - Production/Stable pytest (>=2.3.4) + :pypi:`pytest-poo-fail` Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A + :pypi:`pytest-pook` Pytest plugin for pook Feb 15, 2024 4 - Beta pytest + :pypi:`pytest-pop` A pytest plugin to help with testing pop projects May 09, 2023 5 - Production/Stable pytest + :pypi:`pytest-porringer` Jan 18, 2024 N/A pytest>=7.4.4 + :pypi:`pytest-portion` Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-postgres` Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest + :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. Mar 11, 2024 5 - Production/Stable pytest >=6.2 + :pypi:`pytest-power` pytest plugin with powerful fixtures Dec 31, 2020 N/A pytest (>=5.4) + :pypi:`pytest-powerpack` Mar 17, 2024 N/A pytest (>=8.1.1,<9.0.0) + :pypi:`pytest-prefer-nested-dup-tests` A Pytest plugin to drop duplicated tests during collection, but will prefer keeping nested packages. Apr 27, 2022 4 - Beta pytest (>=7.1.1,<8.0.0) + :pypi:`pytest-pretty` pytest plugin for printing summary data as I want it Apr 05, 2023 5 - Production/Stable pytest>=7 + :pypi:`pytest-pretty-terminal` pytest plugin for generating prettier terminal output Jan 31, 2022 N/A pytest (>=3.4.1) + :pypi:`pytest-pride` Minitest-style test colors Apr 02, 2016 3 - Alpha N/A + :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Aug 25, 2023 5 - Production/Stable pytest>=7.4 + :pypi:`pytest-priority` pytest plugin for add priority for tests Jul 23, 2023 N/A N/A + :pypi:`pytest-proceed` Apr 10, 2024 N/A pytest + :pypi:`pytest-profiles` pytest plugin for configuration profiles Dec 09, 2021 4 - Beta pytest (>=3.7.0) + :pypi:`pytest-profiling` Profiling plugin for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-progress` pytest plugin for instant test progress status Jan 31, 2022 5 - Production/Stable N/A + :pypi:`pytest-prometheus` Report test pass / failures to a Prometheus PushGateway Oct 03, 2017 N/A N/A + :pypi:`pytest-prometheus-pushgateway` Pytest report plugin for Zulip Sep 27, 2022 5 - Production/Stable pytest + :pypi:`pytest-prosper` Test helpers for Prosper projects Sep 24, 2018 N/A N/A + :pypi:`pytest-prysk` Pytest plugin for prysk Mar 12, 2024 4 - Beta pytest (>=7.3.2) + :pypi:`pytest-pspec` A rspec format reporter for Python ptest Jun 02, 2020 4 - Beta pytest (>=3.0.0) + :pypi:`pytest-psqlgraph` pytest plugin for testing applications that use psqlgraph Oct 19, 2021 4 - Beta pytest (>=6.0) + :pypi:`pytest-ptera` Use ptera probes in tests Mar 01, 2022 N/A pytest (>=6.2.4,<7.0.0) + :pypi:`pytest-pudb` Pytest PuDB debugger integration Oct 25, 2018 3 - Alpha pytest (>=2.0) + :pypi:`pytest-pumpkin-spice` A pytest plugin that makes your test reporting pumpkin-spiced Sep 18, 2022 4 - Beta N/A + :pypi:`pytest-purkinje` py.test plugin for purkinje test runner Oct 28, 2017 2 - Pre-Alpha N/A + :pypi:`pytest-pusher` pytest plugin for push report to minio Jan 06, 2023 5 - Production/Stable pytest (>=3.6) + :pypi:`pytest-py125` Dec 03, 2022 N/A N/A + :pypi:`pytest-pycharm` Plugin for py.test to enter PyCharm debugger on uncaught exceptions Aug 13, 2020 5 - Production/Stable pytest (>=2.3) + :pypi:`pytest-pycodestyle` pytest plugin to run pycodestyle Oct 28, 2022 3 - Alpha N/A + :pypi:`pytest-pydev` py.test plugin to connect to a remote debug server with PyDev or PyCharm. Nov 15, 2017 3 - Alpha N/A + :pypi:`pytest-pydocstyle` pytest plugin to run pydocstyle Jan 05, 2023 3 - Alpha N/A + :pypi:`pytest-pylint` pytest plugin to check source code with pylint Oct 06, 2023 5 - Production/Stable pytest >=7.0 + :pypi:`pytest-pymysql-autorecord` Record PyMySQL queries and mock with the stored data. Sep 02, 2022 N/A N/A + :pypi:`pytest-pyodide` Pytest plugin for testing applications that use Pyodide Dec 09, 2023 N/A pytest + :pypi:`pytest-pypi` Easily test your HTTP library against a local copy of pypi Mar 04, 2018 3 - Alpha N/A + :pypi:`pytest-pypom-navigation` Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) + :pypi:`pytest-pyppeteer` A plugin to run pyppeteer in pytest Apr 28, 2022 N/A pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-pyq` Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A + :pypi:`pytest-pyramid` pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite Oct 11, 2023 5 - Production/Stable pytest + :pypi:`pytest-pyramid-server` Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report Feb 03, 2024 N/A pytest + :pypi:`pytest-pyright` Pytest plugin for type checking code with Pyright Jan 26, 2024 4 - Beta pytest >=7.0.0 + :pypi:`pytest-pyspec` A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it". Jan 02, 2024 N/A pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-pystack` Plugin to run pystack after a timeout for a test suite. Jan 04, 2024 N/A pytest >=3.5.0 + :pypi:`pytest-pytestrail` Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) + :pypi:`pytest-pythonhashseed` Pytest plugin to set PYTHONHASHSEED env var. Feb 25, 2024 4 - Beta pytest>=3.0.0 + :pypi:`pytest-pythonpath` pytest plugin for adding to the PYTHONPATH from command line or configs. Feb 10, 2022 5 - Production/Stable pytest (<7,>=2.5.2) + :pypi:`pytest-pytorch` pytest plugin for a better developer experience when working with the PyTorch test suite May 25, 2021 4 - Beta pytest + :pypi:`pytest-pyvenv` A package for create venv in tests Feb 27, 2024 N/A pytest ; extra == 'test' + :pypi:`pytest-pyvista` Pytest-pyvista package Sep 29, 2023 4 - Beta pytest>=3.5.0 + :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration Sep 12, 2023 4 - Beta pytest (>=7.2.2,<8.0.0) + :pypi:`pytest-qasync` Pytest support for qasync. Jul 12, 2021 4 - Beta pytest (>=5.4.0) + :pypi:`pytest-qatouch` Pytest plugin for uploading test results to your QA Touch Testrun. Feb 14, 2023 4 - Beta pytest (>=6.2.0) + :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Nov 29, 2023 5 - Production/Stable pytest >=6.0 + :pypi:`pytest-qml` Run QML Tests with pytest Dec 02, 2020 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-qr` pytest plugin to generate test result QR codes Nov 25, 2021 4 - Beta N/A + :pypi:`pytest-qt` pytest support for PyQt and PySide applications Feb 07, 2024 5 - Production/Stable pytest + :pypi:`pytest-qt-app` QT app fixture for py.test Dec 23, 2015 5 - Production/Stable N/A + :pypi:`pytest-quarantine` A plugin for pytest to manage expected test failures Nov 24, 2019 5 - Production/Stable pytest (>=4.6) + :pypi:`pytest-quickcheck` pytest plugin to generate random data inspired by QuickCheck Nov 05, 2022 4 - Beta pytest (>=4.0) + :pypi:`pytest_quickify` Run test suites with pytest-quickify. Jun 14, 2019 N/A pytest + :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jul 05, 2023 5 - Production/Stable pytest (>=6.2) + :pypi:`pytest-race` Race conditions tester for pytest Jun 07, 2022 4 - Beta N/A + :pypi:`pytest-rage` pytest plugin to implement PEP712 Oct 21, 2011 3 - Alpha N/A + :pypi:`pytest-rail` pytest plugin for creating TestRail runs and adding results May 02, 2022 N/A pytest (>=3.6) + :pypi:`pytest-railflow-testrail-reporter` Generate json reports along with specified metadata defined in test markers. Jun 29, 2022 5 - Production/Stable pytest + :pypi:`pytest-raises` An implementation of pytest.raises as a pytest.mark fixture Apr 23, 2020 N/A pytest (>=3.2.2) + :pypi:`pytest-raisesregexp` Simple pytest plugin to look for regex in Exceptions Dec 18, 2015 N/A N/A + :pypi:`pytest-raisin` Plugin enabling the use of exception instances with pytest.raises Feb 06, 2022 N/A pytest + :pypi:`pytest-random` py.test plugin to randomize tests Apr 28, 2013 3 - Alpha N/A + :pypi:`pytest-randomly` Pytest plugin to randomly order tests and control random.seed. Aug 15, 2023 5 - Production/Stable pytest + :pypi:`pytest-randomness` Pytest plugin about random seed management May 30, 2019 3 - Alpha N/A + :pypi:`pytest-random-num` Randomise the order in which pytest tests are run with some control over the randomness Oct 19, 2020 5 - Production/Stable N/A + :pypi:`pytest-random-order` Randomise the order in which pytest tests are run with some control over the randomness Jan 20, 2024 5 - Production/Stable pytest >=3.0.0 + :pypi:`pytest-ranking` A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection Mar 18, 2024 4 - Beta pytest >=7.4.3 + :pypi:`pytest-readme` Test your README.md file Sep 02, 2022 5 - Production/Stable N/A + :pypi:`pytest-reana` Pytest fixtures for REANA. Mar 14, 2024 3 - Alpha N/A + :pypi:`pytest-recorder` Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. Nov 21, 2023 N/A N/A + :pypi:`pytest-recording` A pytest plugin that allows you recording of network interactions via VCR.py Dec 06, 2023 4 - Beta pytest>=3.5.0 + :pypi:`pytest-recordings` Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal Aug 13, 2020 N/A N/A + :pypi:`pytest-redis` Redis fixtures and fixture factories for Pytest. Apr 19, 2023 5 - Production/Stable pytest (>=6.2) + :pypi:`pytest-redislite` Pytest plugin for testing code using Redis Apr 05, 2022 4 - Beta pytest + :pypi:`pytest-redmine` Pytest plugin for redmine Mar 19, 2018 1 - Planning N/A + :pypi:`pytest-ref` A plugin to store reference files to ease regression testing Nov 23, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-reference-formatter` Conveniently run pytest with a dot-formatted test reference. Oct 01, 2019 4 - Beta N/A + :pypi:`pytest-regex` Select pytest tests with regular expressions May 29, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-regex-dependency` Management of Pytest dependencies via regex patterns Jun 12, 2022 N/A pytest + :pypi:`pytest-regressions` Easy to use fixtures to write regression tests. Aug 31, 2023 5 - Production/Stable pytest >=6.2.0 + :pypi:`pytest-regtest` pytest plugin for snapshot regression testing Feb 26, 2024 N/A pytest>7.2 + :pypi:`pytest-relative-order` a pytest plugin that sorts tests using "before" and "after" markers May 17, 2021 4 - Beta N/A + :pypi:`pytest-relaxed` Relaxed test discovery/organization for pytest Mar 29, 2024 5 - Production/Stable pytest>=7 + :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A + :pypi:`pytest-remotedata` Pytest plugin for controlling remote data access. Sep 26, 2023 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-remote-response` Pytest plugin for capturing and mocking connection requests. Apr 26, 2023 5 - Production/Stable pytest (>=4.6) + :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Jul 07, 2023 4 - Beta pytest + :pypi:`pytest-reorder` Reorder tests depending on their paths and names. May 31, 2018 4 - Beta pytest + :pypi:`pytest-repeat` pytest plugin for repeating tests Oct 09, 2023 5 - Production/Stable pytest + :pypi:`pytest_repeater` py.test plugin for repeating single test multiple times. Feb 09, 2018 1 - Planning N/A + :pypi:`pytest-replay` Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Jan 11, 2024 5 - Production/Stable pytest + :pypi:`pytest-repo-health` A pytest plugin to report on repository standards conformance Apr 17, 2023 3 - Alpha pytest + :pypi:`pytest-report` Creates json report that is compatible with atom.io's linter message format May 11, 2016 4 - Beta N/A + :pypi:`pytest-reporter` Generate Pytest reports with templates Feb 28, 2024 4 - Beta pytest + :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Feb 28, 2024 4 - Beta N/A + :pypi:`pytest-reporter-html-dots` A basic HTML report for pytest using Jinja2 template engine. Jan 22, 2023 N/A N/A + :pypi:`pytest-reportinfra` Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A + :pypi:`pytest-reporting` A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility May 22, 2023 3 - Alpha pytest + :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest + :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) + :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Mar 27, 2024 N/A pytest>=3.8.0 + :pypi:`pytest-report-stream` A pytest plugin which allows to stream test reports at runtime Oct 22, 2023 4 - Beta N/A + :pypi:`pytest-repo-structure` Pytest Repo Structure Mar 18, 2024 1 - Planning N/A + :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) + :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-requestselapsed` collect and show http requests elapsed time Aug 14, 2022 N/A N/A + :pypi:`pytest-requests-futures` Pytest Plugin to Mock Requests Futures Jul 06, 2022 5 - Production/Stable pytest + :pypi:`pytest-requires` A pytest plugin to elegantly skip tests with optional requirements Dec 21, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-reraise` Make multi-threaded pytest test cases fail when they should Sep 20, 2022 5 - Production/Stable pytest (>=4.6) + :pypi:`pytest-rerun` Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) + :pypi:`pytest-rerun-all` Rerun testsuite for a certain time or iterations Nov 16, 2023 3 - Alpha pytest (>=7.0.0) + :pypi:`pytest-rerunclassfailures` pytest rerun class failures plugin Mar 29, 2024 5 - Production/Stable pytest>=7.2 + :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Mar 13, 2024 5 - Production/Stable pytest >=7.2 + :pypi:`pytest-rerunfailures-all-logs` pytest plugin to re-run tests to eliminate flaky failures Mar 07, 2022 5 - Production/Stable N/A + :pypi:`pytest-reserial` Pytest fixture for recording and replaying serial port traffic. Feb 08, 2024 4 - Beta pytest + :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest Apr 03, 2024 N/A pytest~=4.6; python_version == "2.7" + :pypi:`pytest-resource` Load resource fixture plugin to use with pytest Nov 14, 2018 4 - Beta N/A + :pypi:`pytest-resource-path` Provides path for uniform access to test resources in isolated directory May 01, 2021 5 - Production/Stable pytest (>=3.5.0) + :pypi:`pytest-resource-usage` Pytest plugin for reporting running time and peak memory usage Nov 06, 2022 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-responsemock` Simplified requests calls mocking for pytest Mar 10, 2022 5 - Production/Stable N/A + :pypi:`pytest-responses` py.test integration for responses Oct 11, 2022 N/A pytest (>=2.5) + :pypi:`pytest-rest-api` Aug 08, 2022 N/A pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed Jul 10, 2023 5 - Production/Stable pytest + :pypi:`pytest-result-log` A pytest plugin that records the start, end, and result information of each use case in a log file Jan 10, 2024 N/A pytest>=7.2.0 + :pypi:`pytest-result-sender` Apr 20, 2023 N/A pytest>=7.3.1 + :pypi:`pytest-resume` A Pytest plugin to resuming from the last run test Apr 22, 2023 4 - Beta pytest (>=7.0) + :pypi:`pytest-rethinkdb` A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A + :pypi:`pytest-retry` Adds the ability to retry flaky tests in CI environments Feb 04, 2024 N/A pytest >=7.0.0 + :pypi:`pytest-retry-class` A pytest plugin to rerun entire class on failure Mar 25, 2023 N/A pytest (>=5.3) + :pypi:`pytest-reusable-testcases` Apr 28, 2023 N/A N/A + :pypi:`pytest-reverse` Pytest plugin to reverse test order. Jul 10, 2023 5 - Production/Stable pytest + :pypi:`pytest-rich` Leverage rich for richer test session output Mar 03, 2022 4 - Beta pytest (>=7.0) + :pypi:`pytest-richer` Pytest plugin providing a Rich based reporter. Oct 27, 2023 3 - Alpha pytest + :pypi:`pytest-rich-reporter` A pytest plugin using Rich for beautiful test result formatting. Feb 17, 2022 1 - Planning pytest (>=5.0.0) + :pypi:`pytest-richtrace` A pytest plugin that displays the names and information of the pytest hook functions as they are executed. Jun 20, 2023 N/A N/A + :pypi:`pytest-ringo` pytest plugin to test webapplications using the Ringo webframework Sep 27, 2017 3 - Alpha N/A + :pypi:`pytest-rmsis` Sycronise pytest results to Jira RMsis Aug 10, 2022 N/A pytest (>=5.3.5) + :pypi:`pytest-rng` Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest + :pypi:`pytest-roast` pytest plugin for ROAST configuration override and fixtures Nov 09, 2022 5 - Production/Stable pytest + :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Mar 29, 2024 N/A pytest<9,>=7 + :pypi:`pytest-rocketchat` Pytest to Rocket.Chat reporting plugin Apr 18, 2021 5 - Production/Stable N/A + :pypi:`pytest-rotest` Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) + :pypi:`pytest-rpc` Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) + :pypi:`pytest-rst` Test code from RST documents with pytest Jan 26, 2023 N/A N/A + :pypi:`pytest-rt` pytest data collector plugin for Testgr May 05, 2022 N/A N/A + :pypi:`pytest-rts` Coverage-based regression test selection (RTS) plugin for pytest May 17, 2021 N/A pytest + :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Mar 10, 2024 4 - Beta pytest (>=5) + :pypi:`pytest-run-changed` Pytest plugin that runs changed tests only Apr 02, 2021 3 - Alpha pytest + :pypi:`pytest-runfailed` implement a --failed option for pytest Mar 24, 2016 N/A N/A + :pypi:`pytest-run-subprocess` Pytest Plugin for running and testing subprocesses. Nov 12, 2022 5 - Production/Stable pytest + :pypi:`pytest-runtime-types` Checks type annotations on runtime while running tests. Feb 09, 2023 N/A pytest + :pypi:`pytest-runtime-xfail` Call runtime_xfail() to mark running test as xfail. Aug 26, 2021 N/A pytest>=5.0.0 + :pypi:`pytest-runtime-yoyo` run case mark timeout Jun 12, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-saccharin` pytest-saccharin is a updated fork of pytest-sugar, a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Oct 31, 2022 3 - Alpha N/A + :pypi:`pytest-salt` Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A + :pypi:`pytest-salt-containers` A Pytest plugin that builds and creates docker containers Nov 09, 2016 4 - Beta N/A + :pypi:`pytest-salt-factories` Pytest Salt Plugin Mar 22, 2024 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-salt-from-filenames` Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) + :pypi:`pytest-salt-runtests-bridge` Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) + :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) + :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A + :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A + :pypi:`pytest_sauce` pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs Jul 14, 2014 3 - Alpha N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Apr 14, 2024 5 - Production/Stable N/A + :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A + :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A + :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Feb 16, 2024 5 - Production/Stable pytest >=3.5.0 + :pypi:`pytest-screenshot-on-failure` Saves a screenshot when a test case from a pytest execution fails Jul 21, 2023 4 - Beta N/A + :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A + :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) + :pypi:`pytest-selenium` pytest plugin for Selenium Feb 01, 2024 5 - Production/Stable pytest>=6.0.0 + :pypi:`pytest-selenium-auto` pytest plugin to automatically capture screenshots upon selenium webdriver events Nov 07, 2023 N/A pytest >= 7.0.0 + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Apr 14, 2024 5 - Production/Stable N/A + :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A + :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A + :pypi:`pytest-selfie` A pytest plugin for selfie snapshot testing. Apr 05, 2024 N/A pytest<9.0.0,>=8.0.0 + :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A + :pypi:`pytest-sentry` A pytest plugin to send testrun information to Sentry.io Apr 05, 2024 N/A pytest + :pypi:`pytest-sequence-markers` Pytest plugin for sequencing markers for execution of tests May 23, 2023 5 - Production/Stable N/A + :pypi:`pytest-server-fixtures` Extensible server fixures for py.test Dec 19, 2023 5 - Production/Stable pytest + :pypi:`pytest-serverless` Automatically mocks resources from serverless.yml in pytest using moto. May 09, 2022 4 - Beta N/A + :pypi:`pytest-servers` pytest servers Mar 19, 2024 3 - Alpha pytest>=6.2 + :pypi:`pytest-services` Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A + :pypi:`pytest-session2file` pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest + :pypi:`pytest-session-fixture-globalize` py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A + :pypi:`pytest-session_to_file` pytest-session_to_file is a py.test plugin for capturing and saving to file the stdout of py.test. Oct 01, 2015 3 - Alpha N/A + :pypi:`pytest-setupinfo` Displaying setup info during pytest command run Jan 23, 2023 N/A N/A + :pypi:`pytest-sftpserver` py.test plugin to locally test sftp server connections. Sep 16, 2019 4 - Beta N/A + :pypi:`pytest-shard` Dec 11, 2020 4 - Beta pytest + :pypi:`pytest-share-hdf` Plugin to save test data in HDF files and retrieve them for comparison Sep 21, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-sharkreport` this is pytest report plugin. Jul 11, 2022 N/A pytest (>=3.5) + :pypi:`pytest-shell` A pytest plugin to help with testing shell scripts / black box commands Mar 27, 2022 N/A N/A + :pypi:`pytest-shell-utilities` Pytest plugin to simplify running shell commands against the system Feb 23, 2024 5 - Production/Stable pytest >=7.4.0 + :pypi:`pytest-sheraf` Versatile ZODB abstraction layer - pytest fixtures Feb 11, 2020 N/A pytest + :pypi:`pytest-sherlock` pytest plugin help to find coupled tests Aug 14, 2023 5 - Production/Stable pytest >=3.5.1 + :pypi:`pytest-shortcuts` Expand command-line shortcuts listed in pytest configuration Oct 29, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-shutil` A goodie-bag of unix shell and environment tools for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-simbind` Pytest plugin to operate with objects generated by Simbind tool. Mar 28, 2024 N/A pytest>=7.0.0 + :pypi:`pytest-simplehttpserver` Simple pytest fixture to spin up an HTTP server Jun 24, 2021 4 - Beta N/A + :pypi:`pytest-simple-plugin` Simple pytest plugin Nov 27, 2019 N/A N/A + :pypi:`pytest-simple-settings` simple-settings plugin for pytest Nov 17, 2020 4 - Beta pytest + :pypi:`pytest-single-file-logging` Allow for multiple processes to log to a single file May 05, 2016 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-skip-markers` Pytest Salt Plugin Jan 04, 2024 5 - Production/Stable pytest >=7.1.0 + :pypi:`pytest-skipper` A plugin that selects only tests with changes in execution path Mar 26, 2017 3 - Alpha pytest (>=3.0.6) + :pypi:`pytest-skippy` Automatically skip tests that don't need to run! Jan 27, 2018 3 - Alpha pytest (>=2.3.4) + :pypi:`pytest-skip-slow` A pytest plugin to skip \`@pytest.mark.slow\` tests by default. Feb 09, 2023 N/A pytest>=6.2.0 + :pypi:`pytest-skipuntil` A simple pytest plugin to skip flapping test with deadline Nov 25, 2023 4 - Beta pytest >=3.8.0 + :pypi:`pytest-slack` Pytest to Slack reporting plugin Dec 15, 2020 5 - Production/Stable N/A + :pypi:`pytest-slow` A pytest plugin to skip \`@pytest.mark.slow\` tests by default. Sep 28, 2021 N/A N/A + :pypi:`pytest-slowest-first` Sort tests by their last duration, slowest first Dec 11, 2022 4 - Beta N/A + :pypi:`pytest-slow-first` Prioritize running the slowest tests first. Jan 30, 2024 4 - Beta pytest >=3.5.0 + :pypi:`pytest-slow-last` Run tests in order of execution time (faster tests first) Dec 10, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-smartcollect` A plugin for collecting tests that touch changed code Oct 04, 2018 N/A pytest (>=3.5.0) + :pypi:`pytest-smartcov` Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A + :pypi:`pytest-smell` Automated bad smell detection tool for Pytest Jun 26, 2022 N/A N/A + :pypi:`pytest-smtp` Send email with pytest execution result Feb 20, 2021 N/A pytest + :pypi:`pytest-smtp4dev` Plugin for smtp4dev API Jun 27, 2023 5 - Production/Stable N/A + :pypi:`pytest-smtpd` An SMTP server for testing built on aiosmtpd May 15, 2023 N/A pytest + :pypi:`pytest-smtp-test-server` pytest plugin for using \`smtp-test-server\` as a fixture Dec 03, 2023 2 - Pre-Alpha pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-snail` Plugin for adding a marker to slow running tests. 🐌 Nov 04, 2019 3 - Alpha pytest (>=5.0.1) + :pypi:`pytest-snapci` py.test plugin for Snap-CI Nov 12, 2015 N/A N/A + :pypi:`pytest-snapshot` A plugin for snapshot testing with pytest. Apr 23, 2022 4 - Beta pytest (>=3.0.0) + :pypi:`pytest-snapshot-with-message-generator` A plugin for snapshot testing with pytest. Jul 25, 2023 4 - Beta pytest (>=3.0.0) + :pypi:`pytest-snmpserver` May 12, 2021 N/A N/A + :pypi:`pytest-snowflake-bdd` Setup test data and run tests on snowflake in BDD style! Jan 05, 2022 4 - Beta pytest (>=6.2.0) + :pypi:`pytest-socket` Pytest Plugin to disable socket calls during tests Jan 28, 2024 4 - Beta pytest (>=6.2.5) + :pypi:`pytest-sofaepione` Test the installation of SOFA and the SofaEpione plugin. Aug 17, 2022 N/A N/A + :pypi:`pytest-soft-assertions` May 05, 2020 3 - Alpha pytest + :pypi:`pytest-solidity` A PyTest library plugin for Solidity language. Jan 15, 2022 1 - Planning pytest (<7,>=6.0.1) ; extra == 'tests' + :pypi:`pytest-solr` Solr process and client fixtures for py.test. May 11, 2020 3 - Alpha pytest (>=3.0.0) + :pypi:`pytest-sort` Tools for sorting test cases Jan 07, 2024 N/A pytest >=7.4.0 + :pypi:`pytest-sorter` A simple plugin to first execute tests that historically failed more Apr 20, 2021 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-sosu` Unofficial PyTest plugin for Sauce Labs Aug 04, 2023 2 - Pre-Alpha pytest + :pypi:`pytest-sourceorder` Test-ordering plugin for pytest Sep 01, 2021 4 - Beta pytest + :pypi:`pytest-spark` pytest plugin to run the tests with support of pyspark. Feb 23, 2020 4 - Beta pytest + :pypi:`pytest-spawner` py.test plugin to spawn process and communicate with them. Jul 31, 2015 4 - Beta N/A + :pypi:`pytest-spec` Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION. May 04, 2021 N/A N/A + :pypi:`pytest-spec2md` Library pytest-spec2md is a pytest plugin to create a markdown specification while running pytest. Apr 10, 2024 N/A pytest>7.0 + :pypi:`pytest-speed` Modern benchmarking library for python with pytest integration. Jan 22, 2023 3 - Alpha pytest>=7 + :pypi:`pytest-sphinx` Doctest plugin for pytest with support for Sphinx-specific doctest-directives Apr 13, 2024 4 - Beta pytest>=8.1.1 + :pypi:`pytest-spiratest` Exports unit tests as test runs in Spira (SpiraTest/Team/Plan) Jan 01, 2024 N/A N/A + :pypi:`pytest-splinter` Splinter plugin for pytest testing framework Sep 09, 2022 6 - Mature pytest (>=3.0.0) + :pypi:`pytest-splinter4` Pytest plugin for the splinter automation library Feb 01, 2024 6 - Mature pytest >=8.0.0 + :pypi:`pytest-split` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Jan 29, 2024 4 - Beta pytest (>=5,<9) + :pypi:`pytest-split-ext` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Sep 23, 2023 4 - Beta pytest (>=5,<8) + :pypi:`pytest-splitio` Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) + :pypi:`pytest-split-tests` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. Jul 30, 2021 5 - Production/Stable pytest (>=2.5) + :pypi:`pytest-split-tests-tresorit` Feb 22, 2021 1 - Planning N/A + :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Apr 19, 2024 N/A pytest (>5.4.0,<8) + :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Mar 26, 2024 N/A N/A + :pypi:`pytest-splunk-env` pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) + :pypi:`pytest-sqitch` sqitch for pytest Apr 06, 2020 4 - Beta N/A + :pypi:`pytest-sqlalchemy` pytest plugin with sqlalchemy related fixtures Mar 13, 2018 3 - Alpha N/A + :pypi:`pytest-sqlalchemy-mock` pytest sqlalchemy plugin for mock Mar 15, 2023 3 - Alpha pytest (>=2.0) + :pypi:`pytest-sqlalchemy-session` A pytest plugin for preserving test isolation that use SQLAlchemy. May 19, 2023 4 - Beta pytest (>=7.0) + :pypi:`pytest-sql-bigquery` Yet another SQL-testing framework for BigQuery provided by pytest plugin Dec 19, 2019 N/A pytest + :pypi:`pytest-sqlfluff` A pytest plugin to use sqlfluff to enable format checking of sql files. Dec 21, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-squadcast` Pytest report plugin for Squadcast Feb 22, 2022 5 - Production/Stable pytest + :pypi:`pytest-srcpaths` Add paths to sys.path Oct 15, 2021 N/A pytest>=6.2.0 + :pypi:`pytest-ssh` pytest plugin for ssh command run May 27, 2019 N/A pytest + :pypi:`pytest-start-from` Start pytest run from a given point Apr 11, 2016 N/A N/A + :pypi:`pytest-star-track-issue` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A + :pypi:`pytest-static` pytest-static Jan 15, 2024 1 - Planning pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-statsd` pytest plugin for reporting to graphite Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) + :pypi:`pytest-stepfunctions` A small description May 08, 2021 4 - Beta pytest + :pypi:`pytest-steps` Create step-wise / incremental tests in pytest. Sep 23, 2021 5 - Production/Stable N/A + :pypi:`pytest-stepwise` Run a test suite one failing test at a time. Dec 01, 2015 4 - Beta N/A + :pypi:`pytest-stf` pytest plugin for openSTF Mar 25, 2024 N/A pytest>=5.0 + :pypi:`pytest-stoq` A plugin to pytest stoq Feb 09, 2021 4 - Beta N/A + :pypi:`pytest-store` Pytest plugin to store values from test runs Nov 16, 2023 3 - Alpha pytest (>=7.0.0) + :pypi:`pytest-stress` A Pytest plugin that allows you to loop tests for a user defined amount of time. Dec 07, 2019 4 - Beta pytest (>=3.6.0) + :pypi:`pytest-structlog` Structured logging assertions Mar 13, 2024 N/A pytest + :pypi:`pytest-structmpd` provide structured temporary directory Oct 17, 2018 N/A N/A + :pypi:`pytest-stub` Stub packages, modules and attributes. Apr 28, 2020 5 - Production/Stable N/A + :pypi:`pytest-stubprocess` Provide stub implementations for subprocesses in Python tests Sep 17, 2018 3 - Alpha pytest (>=3.5.0) + :pypi:`pytest-study` A pytest plugin to organize long run tests (named studies) without interfering the regular tests Sep 26, 2017 3 - Alpha pytest (>=2.0) + :pypi:`pytest-subinterpreter` Run pytest in a subinterpreter Nov 25, 2023 N/A pytest>=7.0.0 + :pypi:`pytest-subprocess` A plugin to fake subprocess for pytest Jan 28, 2023 5 - Production/Stable pytest (>=4.0.0) + :pypi:`pytest-subtesthack` A hack to explicitly set up and tear down fixtures. Jul 16, 2022 N/A N/A + :pypi:`pytest-subtests` unittest subTest() support and subtests fixture Mar 07, 2024 4 - Beta pytest >=7.0 + :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Sep 17, 2023 N/A pytest (>=2.3) + :pypi:`pytest-sugar` pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Feb 01, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-suitemanager` A simple plugin to use with pytest Apr 28, 2023 4 - Beta N/A + :pypi:`pytest-suite-timeout` A pytest plugin for ensuring max suite time Jan 26, 2024 N/A pytest>=7.0.0 + :pypi:`pytest-supercov` Pytest plugin for measuring explicit test-file to source-file coverage Jul 02, 2023 N/A N/A + :pypi:`pytest-svn` SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-symbols` pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A + :pypi:`pytest-synodic` Synodic Pytest utilities Mar 09, 2024 N/A pytest>=8.0.2 + :pypi:`pytest-system-statistics` Pytest plugin to track and report system usage statistics Feb 16, 2022 5 - Production/Stable pytest (>=6.0.0) + :pypi:`pytest-system-test-plugin` Pyst - Pytest System-Test Plugin Feb 03, 2022 N/A N/A + :pypi:`pytest_tagging` a pytest plugin to tag tests Apr 08, 2024 N/A pytest<8.0.0,>=7.1.3 + :pypi:`pytest-takeltest` Fixtures for ansible, testinfra and molecule Feb 15, 2023 N/A N/A + :pypi:`pytest-talisker` Nov 28, 2021 N/A N/A + :pypi:`pytest-tally` A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. May 22, 2023 4 - Beta pytest (>=6.2.5) + :pypi:`pytest-tap` Test Anything Protocol (TAP) reporting plugin for pytest Jul 15, 2023 5 - Production/Stable pytest (>=3.0) + :pypi:`pytest-tape` easy assertion with expected results saved to yaml files Mar 17, 2021 4 - Beta N/A + :pypi:`pytest-target` Pytest plugin for remote target orchestration. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) + :pypi:`pytest-tblineinfo` tblineinfo is a py.test plugin that insert the node id in the final py.test report when --tb=line option is used Dec 01, 2015 3 - Alpha pytest (>=2.0) + :pypi:`pytest-tcpclient` A pytest plugin for testing TCP clients Nov 16, 2022 N/A pytest (<8,>=7.1.3) + :pypi:`pytest-tdd` run pytest on a python module Aug 18, 2023 4 - Beta N/A + :pypi:`pytest-teamcity-logblock` py.test plugin to introduce block structure in teamcity build log, if output is not captured May 15, 2018 4 - Beta N/A + :pypi:`pytest-telegram` Pytest to Telegram reporting plugin Dec 10, 2020 5 - Production/Stable N/A + :pypi:`pytest-telegram-notifier` Telegram notification plugin for Pytest Jun 27, 2023 5 - Production/Stable N/A + :pypi:`pytest-tempdir` Predictable and repeatable tempdir support. Oct 11, 2019 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-terra-fixt` Terraform and Terragrunt fixtures for pytest Sep 15, 2022 N/A pytest (==6.2.5) + :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures Jun 20, 2023 N/A pytest (>=6.0) + :pypi:`pytest-terraform-fixture` generate terraform resources to use with pytest Nov 14, 2018 4 - Beta N/A + :pypi:`pytest-testbook` A plugin to run tests written in Jupyter notebook Dec 11, 2016 3 - Alpha N/A + :pypi:`pytest-testconfig` Test configuration plugin for pytest. Jan 11, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-testdirectory` A py.test plugin providing temporary directories in unit tests. May 02, 2023 5 - Production/Stable pytest + :pypi:`pytest-testdox` A testdox format reporter for pytest Jul 22, 2023 5 - Production/Stable pytest (>=4.6.0) + :pypi:`pytest-test-grouping` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Feb 01, 2023 5 - Production/Stable pytest (>=2.5) + :pypi:`pytest-test-groups` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Oct 25, 2016 5 - Production/Stable N/A + :pypi:`pytest-testinfra` Test infrastructures Feb 15, 2024 5 - Production/Stable pytest >=6 + :pypi:`pytest-testinfra-jpic` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A + :pypi:`pytest-testinfra-winrm-transport` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A + :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) + :pypi:`pytest-testmon` selects tests affected by changed files and methods Feb 27, 2024 4 - Beta pytest <9,>=5 + :pypi:`pytest-testmon-dev` selects tests affected by changed files and methods Mar 30, 2023 4 - Beta pytest (<8,>=5) + :pypi:`pytest-testmon-oc` nOly selects tests affected by changed files and methods Jun 01, 2022 4 - Beta pytest (<8,>=5) + :pypi:`pytest-testmon-skip-libraries` selects tests affected by changed files and methods Mar 03, 2023 4 - Beta pytest (<8,>=5) + :pypi:`pytest-testobject` Plugin to use TestObject Suites with Pytest Sep 24, 2019 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-testpluggy` set your encoding Jan 07, 2022 N/A pytest + :pypi:`pytest-testrail` pytest plugin for creating TestRail runs and adding results Aug 27, 2020 N/A pytest (>=3.6) + :pypi:`pytest-testrail2` A pytest plugin to upload results to TestRail. Feb 10, 2023 N/A pytest (<8.0,>=7.2.0) + :pypi:`pytest-testrail-api-client` TestRail Api Python Client Dec 14, 2021 N/A pytest + :pypi:`pytest-testrail-appetize` pytest plugin for creating TestRail runs and adding results Sep 29, 2021 N/A N/A + :pypi:`pytest-testrail-client` pytest plugin for Testrail Sep 29, 2020 5 - Production/Stable N/A + :pypi:`pytest-testrail-e2e` pytest plugin for creating TestRail runs and adding results Oct 11, 2021 N/A pytest (>=3.6) + :pypi:`pytest-testrail-integrator` Pytest plugin for sending report to testrail system. Aug 01, 2022 N/A pytest (>=6.2.5) + :pypi:`pytest-testrail-ns` pytest plugin for creating TestRail runs and adding results Aug 12, 2022 N/A N/A + :pypi:`pytest-testrail-plugin` PyTest plugin for TestRail Apr 21, 2020 3 - Alpha pytest + :pypi:`pytest-testrail-reporter` Sep 10, 2018 N/A N/A + :pypi:`pytest-testrail-results` A pytest plugin to upload results to TestRail. Mar 04, 2024 N/A pytest >=7.2.0 + :pypi:`pytest-testreport` Dec 01, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-testreport-new` Oct 07, 2023 4 - Beta pytest >=3.5.0 + :pypi:`pytest-testslide` TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) + :pypi:`pytest-test-this` Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply Sep 15, 2019 2 - Pre-Alpha pytest (>=2.3) + :pypi:`pytest-test-utils` Feb 08, 2024 N/A pytest >=3.9 + :pypi:`pytest-tesults` Tesults plugin for pytest Feb 15, 2024 5 - Production/Stable pytest >=3.5.0 + :pypi:`pytest-textual-snapshot` Snapshot testing for Textual apps Aug 23, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A + :pypi:`pytest-th2-bdd` pytest_th2_bdd May 13, 2022 N/A N/A + :pypi:`pytest-thawgun` Pytest plugin for time travel May 26, 2020 3 - Alpha N/A + :pypi:`pytest-thread` Jul 07, 2023 N/A N/A + :pypi:`pytest-threadleak` Detects thread leaks Jul 03, 2022 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-tick` Ticking on tests Aug 31, 2021 5 - Production/Stable pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-time` Jun 24, 2023 3 - Alpha pytest + :pypi:`pytest-timeassert-ethan` execution duration Dec 25, 2023 N/A pytest + :pypi:`pytest-timeit` A pytest plugin to time test function runs Oct 13, 2016 4 - Beta N/A + :pypi:`pytest-timeout` pytest plugin to abort hanging tests Mar 07, 2024 5 - Production/Stable pytest >=7.0.0 + :pypi:`pytest-timeouts` Linux-only Pytest plugin to control durations of various test case execution phases Sep 21, 2019 5 - Production/Stable N/A + :pypi:`pytest-timer` A timer plugin for pytest Dec 26, 2023 N/A pytest + :pypi:`pytest-timestamper` Pytest plugin to add a timestamp prefix to the pytest output Mar 27, 2024 N/A N/A + :pypi:`pytest-timestamps` A simple plugin to view timestamps for each test Sep 11, 2023 N/A pytest (>=7.3,<8.0) + :pypi:`pytest-tiny-api-client` The companion pytest plugin for tiny-api-client Jan 04, 2024 5 - Production/Stable pytest + :pypi:`pytest-tinybird` A pytest plugin to report test results to tinybird Jun 26, 2023 4 - Beta pytest (>=3.8.0) + :pypi:`pytest-tipsi-django` Better fixtures for django Feb 05, 2024 5 - Production/Stable pytest>=6.0.0 + :pypi:`pytest-tipsi-testing` Better fixtures management. Various helpers Feb 04, 2024 5 - Production/Stable pytest>=3.3.0 + :pypi:`pytest-tldr` A pytest plugin that limits the output to just the things you need. Oct 26, 2022 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-tm4j-reporter` Cloud Jira Test Management (TM4J) PyTest reporter plugin Sep 01, 2020 N/A pytest + :pypi:`pytest-tmnet` A small example package Mar 01, 2022 N/A N/A + :pypi:`pytest-tmp-files` Utilities to create temporary file hierarchies in pytest. Dec 08, 2023 N/A pytest + :pypi:`pytest-tmpfs` A pytest plugin that helps you on using a temporary filesystem for testing. Aug 29, 2022 N/A pytest + :pypi:`pytest-tmreport` this is a vue-element ui report for pytest Aug 12, 2022 N/A N/A + :pypi:`pytest-tmux` A pytest plugin that enables tmux driven tests Apr 22, 2023 4 - Beta N/A + :pypi:`pytest-todo` A small plugin for the pytest testing framework, marking TODO comments as failure May 23, 2019 4 - Beta pytest + :pypi:`pytest-tomato` Mar 01, 2019 5 - Production/Stable N/A + :pypi:`pytest-toolbelt` This is just a collection of utilities for pytest, but don't really belong in pytest proper. Aug 12, 2019 3 - Alpha N/A + :pypi:`pytest-toolbox` Numerous useful plugins for pytest. Apr 07, 2018 N/A pytest (>=3.5.0) + :pypi:`pytest-toolkit` Useful utils for testing Apr 13, 2024 N/A N/A + :pypi:`pytest-tools` Pytest tools Oct 21, 2022 4 - Beta N/A + :pypi:`pytest-tornado` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Jun 17, 2020 5 - Production/Stable pytest (>=3.6) + :pypi:`pytest-tornado5` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Nov 16, 2018 5 - Production/Stable pytest (>=3.6) + :pypi:`pytest-tornado-yen3` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Oct 15, 2018 5 - Production/Stable N/A + :pypi:`pytest-tornasync` py.test plugin for testing Python 3.5+ Tornado code Jul 15, 2019 3 - Alpha pytest (>=3.0) + :pypi:`pytest-trace` Save OpenTelemetry spans generated during testing Jun 19, 2022 N/A pytest (>=4.6) + :pypi:`pytest-track` Feb 26, 2021 3 - Alpha pytest (>=3.0) + :pypi:`pytest-translations` Test your translation files. Sep 11, 2023 5 - Production/Stable pytest (>=7) + :pypi:`pytest-travis-fold` Folds captured output sections in Travis CI build log Nov 29, 2017 4 - Beta pytest (>=2.6.0) + :pypi:`pytest-trello` Plugin for py.test that integrates trello using markers Nov 20, 2015 5 - Production/Stable N/A + :pypi:`pytest-trepan` Pytest plugin for trepan debugger. Jul 28, 2018 5 - Production/Stable N/A + :pypi:`pytest-trialtemp` py.test plugin for using the same _trial_temp working directory as trial Jun 08, 2015 N/A N/A + :pypi:`pytest-trio` Pytest plugin for trio Nov 01, 2022 N/A pytest (>=7.2.0) + :pypi:`pytest-trytond` Pytest plugin for the Tryton server framework Nov 04, 2022 4 - Beta pytest (>=5) + :pypi:`pytest-tspwplib` A simple plugin to use with tspwplib Jan 08, 2021 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-tst` Customize pytest options, output and exit code to make it compatible with tst Apr 27, 2022 N/A pytest (>=5.0.0) + :pypi:`pytest-tstcls` Test Class Base Mar 23, 2020 5 - Production/Stable N/A + :pypi:`pytest-tui` Text User Interface (TUI) and HTML report for Pytest test runs Dec 08, 2023 4 - Beta N/A + :pypi:`pytest-tutorials` Mar 11, 2023 N/A N/A + :pypi:`pytest-twilio-conversations-client-mock` Aug 02, 2022 N/A N/A + :pypi:`pytest-twisted` A twisted plugin for pytest. Mar 19, 2024 5 - Production/Stable pytest >=2.3 + :pypi:`pytest-typechecker` Run type checkers on specified test files Feb 04, 2022 N/A pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-typhoon-config` A Typhoon HIL plugin that facilitates test parameter configuration at runtime Apr 07, 2022 5 - Production/Stable N/A + :pypi:`pytest-typhoon-polarion` Typhoontest plugin for Siemens Polarion Feb 01, 2024 4 - Beta N/A + :pypi:`pytest-typhoon-xray` Typhoon HIL plugin for pytest Aug 15, 2023 4 - Beta N/A + :pypi:`pytest-tytest` Typhoon HIL plugin for pytest May 25, 2020 4 - Beta pytest (>=5.4.2) + :pypi:`pytest-ubersmith` Easily mock calls to ubersmith at the \`requests\` level. Apr 13, 2015 N/A N/A + :pypi:`pytest-ui` Text User Interface for running python tests Jul 05, 2021 4 - Beta pytest + :pypi:`pytest-ui-failed-screenshot` UI自动测试失败时自动截图,并将截图加入到测试报告中 Dec 06, 2022 N/A N/A + :pypi:`pytest-ui-failed-screenshot-allure` UI自动测试失败时自动截图,并将截图加入到Allure测试报告中 Dec 06, 2022 N/A N/A + :pypi:`pytest-uncollect-if` A plugin to uncollect pytests tests rather than using skipif Mar 24, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-unflakable` Unflakable plugin for PyTest Nov 12, 2023 4 - Beta pytest >=6.2.0 + :pypi:`pytest-unhandled-exception-exit-code` Plugin for py.test set a different exit code on uncaught exceptions Jun 22, 2020 5 - Production/Stable pytest (>=2.3) + :pypi:`pytest-unique` Pytest fixture to generate unique values. Sep 15, 2023 N/A pytest (>=7.4.2,<8.0.0) + :pypi:`pytest-unittest-filter` A pytest plugin for filtering unittest-based test classes Jan 12, 2019 4 - Beta pytest (>=3.1.0) + :pypi:`pytest-unmarked` Run only unmarked tests Aug 27, 2019 5 - Production/Stable N/A + :pypi:`pytest-unordered` Test equality of unordered collections in pytest Mar 13, 2024 4 - Beta pytest >=7.0.0 + :pypi:`pytest-unstable` Set a test as unstable to return 0 even if it failed Sep 27, 2022 4 - Beta N/A + :pypi:`pytest-unused-fixtures` A pytest plugin to list unused fixtures after a test run. Apr 08, 2024 4 - Beta pytest>7.3.2 + :pypi:`pytest-upload-report` pytest-upload-report is a plugin for pytest that upload your test report for test results. Jun 18, 2021 5 - Production/Stable N/A + :pypi:`pytest-utils` Some helpers for pytest. Feb 02, 2023 4 - Beta pytest (>=7.0.0,<8.0.0) + :pypi:`pytest-vagrant` A py.test plugin providing access to vagrant. Sep 07, 2021 5 - Production/Stable pytest + :pypi:`pytest-valgrind` May 19, 2021 N/A N/A + :pypi:`pytest-variables` pytest plugin for providing variables to tests/fixtures Feb 01, 2024 5 - Production/Stable pytest>=7.0.0 + :pypi:`pytest-variant` Variant support for Pytest Jun 06, 2022 N/A N/A + :pypi:`pytest-vcr` Plugin for managing VCR.py cassettes Apr 26, 2019 5 - Production/Stable pytest (>=3.6.0) + :pypi:`pytest-vcr-delete-on-fail` A pytest plugin that automates vcrpy cassettes deletion on test failure. Feb 16, 2024 5 - Production/Stable pytest (>=8.0.0,<9.0.0) + :pypi:`pytest-vcrpandas` Test from HTTP interactions to dataframe processed. Jan 12, 2019 4 - Beta pytest + :pypi:`pytest-vcs` Sep 22, 2022 4 - Beta N/A + :pypi:`pytest-venv` py.test fixture for creating a virtual environment Nov 23, 2023 4 - Beta pytest + :pypi:`pytest-ver` Pytest module with Verification Protocol, Verification Report and Trace Matrix Feb 07, 2024 4 - Beta pytest + :pypi:`pytest-verbose-parametrize` More descriptive output for parametrized py.test tests May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-vimqf` A simple pytest plugin that will shrink pytest output when specified, to fit vim quickfix window. Feb 08, 2021 4 - Beta pytest (>=6.2.2,<7.0.0) + :pypi:`pytest-virtualenv` Virtualenv fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-visual` Nov 01, 2023 3 - Alpha pytest >=7.0.0 + :pypi:`pytest-vnc` VNC client for Pytest Nov 06, 2023 N/A pytest + :pypi:`pytest-voluptuous` Pytest plugin for asserting data against voluptuous schema. Jun 09, 2020 N/A pytest + :pypi:`pytest-vscodedebug` A pytest plugin to easily enable debugging tests within Visual Studio Code Dec 04, 2020 4 - Beta N/A + :pypi:`pytest-vscode-pycharm-cls` A PyTest helper to enable start remote debugger on test start or failure or when pytest.set_trace is used. Feb 01, 2023 N/A pytest + :pypi:`pytest-vts` pytest plugin for automatic recording of http stubbed tests Jun 05, 2019 N/A pytest (>=2.3) + :pypi:`pytest-vulture` A pytest plugin to checks dead code with vulture Jun 01, 2023 N/A pytest (>=7.0.0) + :pypi:`pytest-vw` pytest-vw makes your failing test cases succeed under CI tools scrutiny Oct 07, 2015 4 - Beta N/A + :pypi:`pytest-vyper` Plugin for the vyper smart contract language. May 28, 2020 2 - Pre-Alpha N/A + :pypi:`pytest-wa-e2e-plugin` Pytest plugin for testing whatsapp bots with end to end tests Feb 18, 2020 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-wake` Mar 20, 2024 N/A pytest + :pypi:`pytest-watch` Local continuous test runner with pytest and watchdog. May 20, 2018 N/A N/A + :pypi:`pytest-watcher` Automatically rerun your tests on file modifications Apr 01, 2024 4 - Beta N/A + :pypi:`pytest_wdb` Trace pytest tests with wdb to halt on error with --wdb. Jul 04, 2016 N/A N/A + :pypi:`pytest-wdl` Pytest plugin for testing WDL workflows. Nov 17, 2020 5 - Production/Stable N/A + :pypi:`pytest-web3-data` A pytest plugin to fetch test data from IPFS HTTP gateways during pytest execution. Oct 04, 2023 4 - Beta pytest + :pypi:`pytest-webdriver` Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-webtest-extras` Pytest plugin to enhance pytest-html and allure reports of webtest projects by adding screenshots, comments and webpage sources. Nov 13, 2023 N/A pytest >= 7.0.0 + :pypi:`pytest-wetest` Welian API Automation test framework pytest plugin Nov 10, 2018 4 - Beta N/A + :pypi:`pytest-when` Utility which makes mocking more readable and controllable Mar 22, 2024 N/A pytest>=7.3.1 + :pypi:`pytest-whirlwind` Testing Tornado. Jun 12, 2020 N/A N/A + :pypi:`pytest-wholenodeid` pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) + :pypi:`pytest-win32consoletitle` Pytest progress in console title (Win32 only) Aug 08, 2021 N/A N/A + :pypi:`pytest-winnotify` Windows tray notifications for py.test results. Apr 22, 2016 N/A N/A + :pypi:`pytest-wiremock` A pytest plugin for programmatically using wiremock in integration tests Mar 27, 2022 N/A pytest (>=7.1.1,<8.0.0) + :pypi:`pytest-with-docker` pytest with docker helpers. Nov 09, 2021 N/A pytest + :pypi:`pytest-workflow` A pytest plugin for configuring workflow/pipeline tests using YAML files Mar 18, 2024 5 - Production/Stable pytest >=7.0.0 + :pypi:`pytest-xdist` pytest xdist plugin for distributed testing, most importantly across multiple CPUs Apr 19, 2024 5 - Production/Stable pytest >=6.2.0 + :pypi:`pytest-xdist-debug-for-graingert` pytest xdist plugin for distributed testing and loop-on-failing modes Jul 24, 2019 5 - Production/Stable pytest (>=4.4.0) + :pypi:`pytest-xdist-forked` forked from pytest-xdist Feb 10, 2020 5 - Production/Stable pytest (>=4.4.0) + :pypi:`pytest-xdist-tracker` pytest plugin helps to reproduce failures for particular xdist node Nov 18, 2021 3 - Alpha pytest (>=3.5.1) + :pypi:`pytest-xdist-worker-stats` A pytest plugin to list worker statistics after a xdist run. Apr 16, 2024 4 - Beta pytest>=7.0.0 + :pypi:`pytest-xfaillist` Maintain a xfaillist in an additional file to avoid merge-conflicts. Sep 17, 2021 N/A pytest (>=6.2.2,<7.0.0) + :pypi:`pytest-xfiles` Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A + :pypi:`pytest-xiuyu` This is a pytest plugin Jul 25, 2023 5 - Production/Stable N/A + :pypi:`pytest-xlog` Extended logging for test and decorators May 31, 2020 4 - Beta N/A + :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Mar 22, 2024 N/A N/A + :pypi:`pytest-xpara` An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest + :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. Mar 31, 2024 4 - Beta pytest>=2.8 + :pypi:`pytest-xray` May 30, 2019 3 - Alpha N/A + :pypi:`pytest-xrayjira` Mar 17, 2020 3 - Alpha pytest (==4.3.1) + :pypi:`pytest-xray-server` May 03, 2022 3 - Alpha pytest (>=5.3.1) + :pypi:`pytest-xskynet` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A + :pypi:`pytest-xvfb` A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. May 29, 2023 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-xvirt` A pytest plugin to virtualize test. For example to transparently running them on a remote box. Oct 01, 2023 4 - Beta pytest >=7.1.0 + :pypi:`pytest-yaml` This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest + :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Apr 19, 2024 N/A pytest>=7.4.0 + :pypi:`pytest-yamltree` Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-yamlwsgi` Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A + :pypi:`pytest-yaml-yoyo` http/https API run by yaml Jun 19, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-yapf` Run yapf Jul 06, 2017 4 - Beta pytest (>=3.1.1) + :pypi:`pytest-yapf3` Validate your Python file format with yapf Mar 29, 2023 5 - Production/Stable pytest (>=7) + :pypi:`pytest-yield` PyTest plugin to run tests concurrently, each \`yield\` switch context to other one Jan 23, 2019 N/A N/A + :pypi:`pytest-yls` Pytest plugin to test the YLS as a whole. Mar 30, 2024 N/A pytest<8.0.0,>=7.2.2 + :pypi:`pytest-yuk` Display tests you are uneasy with, using 🤢/🤮 for pass/fail of tests marked with yuk. Mar 26, 2021 N/A pytest>=5.0.0 + :pypi:`pytest-zafira` A Zafira plugin for pytest Sep 18, 2019 5 - Production/Stable pytest (==4.1.1) + :pypi:`pytest-zap` OWASP ZAP plugin for py.test. May 12, 2014 4 - Beta N/A + :pypi:`pytest-zebrunner` Pytest connector for Zebrunner reporting Jan 08, 2024 5 - Production/Stable pytest (>=4.5.0) + :pypi:`pytest-zeebe` Pytest fixtures for testing Camunda 8 processes using a Zeebe test engine. Feb 01, 2024 N/A pytest (>=7.4.2,<8.0.0) + :pypi:`pytest-zest` Zesty additions to pytest. Nov 17, 2022 N/A N/A + :pypi:`pytest-zhongwen-wendang` PyTest 中文文档 Mar 04, 2024 4 - Beta N/A + :pypi:`pytest-zigzag` Extend py.test for RPC OpenStack testing. Feb 27, 2019 4 - Beta pytest (~=3.6) + :pypi:`pytest-zulip` Pytest report plugin for Zulip May 07, 2022 5 - Production/Stable pytest + :pypi:`pytest-zy` 接口自动化测试框架 Mar 24, 2024 N/A pytest~=7.2.0 + =============================================== ====================================================================================================================================================================================================================================================================================================================================================================================== ============== ===================== ================================================ .. only:: latex + :pypi:`logassert` + *last release*: May 20, 2022, + *status*: 5 - Production/Stable, + *requires*: N/A + + Simple but powerful assertion and verification of logged lines. + + :pypi:`logot` + *last release*: Mar 23, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest<9,>=7; extra == "pytest" + + Test whether your code is logging correctly 🪵 + + :pypi:`nuts` + *last release*: Aug 11, 2023, + *status*: N/A, + *requires*: pytest (>=7.3.0,<8.0.0) + + Network Unit Testing System + + :pypi:`pytest-abq` + *last release*: Apr 07, 2023, + *status*: N/A, + *requires*: N/A + + Pytest integration for the ABQ universal test runner. + + :pypi:`pytest-abstracts` + *last release*: May 25, 2022, + *status*: N/A, + *requires*: N/A + + A contextmanager pytest fixture for handling multiple mock abstracts + :pypi:`pytest-accept` - *last release*: Nov 22, 2021, + *last release*: Feb 10, 2024, *status*: N/A, - *requires*: pytest (>=6,<7) + *requires*: pytest (>=6) A pytest-plugin for updating doctest outputs :pypi:`pytest-adaptavist` - *last release*: Nov 30, 2021, + *last release*: Oct 13, 2022, *status*: N/A, *requires*: pytest (>=5.4.0) pytest plugin for generating test execution results within Jira Test Management (tm4j) + :pypi:`pytest-adaptavist-fixed` + *last release*: Nov 08, 2023, + *status*: N/A, + *requires*: pytest >=5.4.0 + + pytest plugin for generating test execution results within Jira Test Management (tm4j) + :pypi:`pytest-addons-test` *last release*: Aug 02, 2021, *status*: N/A, @@ -1021,6 +1564,20 @@ This list contains 963 plugins. Pytest plugin for writing Azure Data Factory integration tests + :pypi:`pytest-ads-testplan` + *last release*: Sep 15, 2022, + *status*: N/A, + *requires*: N/A + + Azure DevOps Test Case reporting for pytest tests + + :pypi:`pytest-affected` + *last release*: Nov 06, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-agent` *last release*: Nov 25, 2021, *status*: N/A, @@ -1035,9 +1592,16 @@ This list contains 963 plugins. pytest plugin for pytest-repeat that generate aggregate report of the same test cases with additional statistics details. + :pypi:`pytest-ai1899` + *last release*: Mar 13, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + pytest plugin for connecting to ai1899 smart system stack + :pypi:`pytest-aio` - *last release*: Oct 20, 2021, - *status*: 4 - Beta, + *last release*: Apr 08, 2024, + *status*: 5 - Production/Stable, *requires*: pytest Pytest plugin for testing async python code @@ -1049,20 +1613,34 @@ This list contains 963 plugins. pytest fixtures for writing aiofiles tests with pyfakefs - :pypi:`pytest-aiohttp` - *last release*: Dec 05, 2017, + :pypi:`pytest-aiogram` + *last release*: May 06, 2023, *status*: N/A, - *requires*: pytest + *requires*: N/A + - pytest plugin for aiohttp support + + :pypi:`pytest-aiohttp` + *last release*: Sep 06, 2023, + *status*: 4 - Beta, + *requires*: pytest >=6.1.0 + + Pytest plugin for aiohttp support :pypi:`pytest-aiohttp-client` - *last release*: Nov 01, 2020, + *last release*: Jan 10, 2023, *status*: N/A, - *requires*: pytest (>=6) + *requires*: pytest (>=7.2.0,<8.0.0) Pytest \`client\` fixture for the Aiohttp + :pypi:`pytest-aiomoto` + *last release*: Jun 24, 2023, + *status*: N/A, + *requires*: pytest (>=7.0,<8.0) + + pytest-aiomoto + :pypi:`pytest-aioresponses` *last release*: Jul 29, 2021, *status*: 4 - Beta, @@ -1071,9 +1649,9 @@ This list contains 963 plugins. py.test integration for aioresponses :pypi:`pytest-aioworkers` - *last release*: Dec 04, 2019, - *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *last release*: May 01, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest>=6.1.0 A plugin to test aioworkers project with pytest @@ -1092,9 +1670,9 @@ This list contains 963 plugins. :pypi:`pytest-alembic` - *last release*: Dec 02, 2021, + *last release*: Mar 04, 2024, *status*: N/A, - *requires*: pytest (>=1.0) + *requires*: pytest (>=6.0) A pytest plugin for verifying alembic migrations. @@ -1119,6 +1697,13 @@ This list contains 963 plugins. Plugin for py.test to generate allure xml reports + :pypi:`pytest-allure-collection` + *last release*: Apr 13, 2023, + *status*: N/A, + *requires*: pytest + + pytest plugin to collect allure markers without running any tests + :pypi:`pytest-allure-dsl` *last release*: Oct 25, 2020, *status*: 4 - Beta, @@ -1126,6 +1711,13 @@ This list contains 963 plugins. pytest plugin to test case doc string dls instructions + :pypi:`pytest-allure-intersection` + *last release*: Oct 27, 2022, + *status*: N/A, + *requires*: pytest (<5) + + + :pypi:`pytest-allure-spec-coverage` *last release*: Oct 26, 2021, *status*: N/A, @@ -1134,12 +1726,19 @@ This list contains 963 plugins. The pytest plugin aimed to display test coverage of the specs(requirements) in Allure :pypi:`pytest-alphamoon` - *last release*: Oct 21, 2021, - *status*: 4 - Beta, + *last release*: Dec 30, 2021, + *status*: 5 - Production/Stable, *requires*: pytest (>=3.5.0) Static code checks used at Alphamoon + :pypi:`pytest-analyzer` + *last release*: Feb 21, 2024, + *status*: N/A, + *requires*: pytest <8.0.0,>=7.3.1 + + this plugin allows to analyze tests in pytest project, collect test metadata and sync it with testomat.io TCM system + :pypi:`pytest-android` *last release*: Feb 21, 2019, *status*: 3 - Alpha, @@ -1148,25 +1747,25 @@ This list contains 963 plugins. This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. :pypi:`pytest-anki` - *last release*: Oct 14, 2021, + *last release*: Jul 31, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) A pytest plugin for testing Anki add-ons :pypi:`pytest-annotate` - *last release*: Nov 29, 2021, + *last release*: Jun 07, 2022, *status*: 3 - Alpha, - *requires*: pytest (<7.0.0,>=3.2.0) + *requires*: pytest (<8.0.0,>=3.2.0) pytest-annotate: Generate PyAnnotate annotations from your pytest tests. :pypi:`pytest-ansible` - *last release*: May 25, 2021, + *last release*: Jan 18, 2024, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest >=6 - Plugin for py.test to simplify calling ansible modules from tests or fixtures + Plugin for pytest to simplify calling ansible modules from tests or fixtures :pypi:`pytest-ansible-playbook` *last release*: Mar 08, 2019, @@ -1182,8 +1781,15 @@ This list contains 963 plugins. Pytest fixture which runs given ansible playbook file. + :pypi:`pytest-ansible-units` + *last release*: Apr 14, 2022, + *status*: N/A, + *requires*: N/A + + A pytest plugin for running unit tests within an ansible collection + :pypi:`pytest-antilru` - *last release*: Apr 11, 2019, + *last release*: Jul 05, 2022, *status*: 5 - Production/Stable, *requires*: pytest @@ -1197,25 +1803,39 @@ This list contains 963 plugins. The pytest anyio plugin is built into anyio. You don't need this package. :pypi:`pytest-anything` - *last release*: Feb 18, 2021, + *last release*: Jan 18, 2024, *status*: N/A, - *requires*: N/A + *requires*: pytest Pytest fixtures to assert anything and something :pypi:`pytest-aoc` - *last release*: Nov 23, 2021, - *status*: N/A, + *last release*: Dec 02, 2023, + *status*: 5 - Production/Stable, *requires*: pytest ; extra == 'test' Downloads puzzle inputs for Advent of Code and synthesizes PyTest fixtures + :pypi:`pytest-aoreporter` + *last release*: Jun 27, 2022, + *status*: N/A, + *requires*: N/A + + pytest report + :pypi:`pytest-api` - *last release*: May 04, 2021, + *last release*: May 12, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.1,<8.0.0) + + An ASGI middleware to populate OpenAPI Specification examples from pytest functions + + :pypi:`pytest-api-soup` + *last release*: Aug 27, 2022, *status*: N/A, *requires*: N/A - PyTest-API Python Web Framework built for testing purposes. + Validate multiple endpoints with unit testing using a single source of truth. :pypi:`pytest-apistellar` *last release*: Jun 18, 2019, @@ -1239,12 +1859,26 @@ This list contains 963 plugins. Pytest plugin for appium :pypi:`pytest-approvaltests` - *last release*: Feb 07, 2021, + *last release*: May 08, 2022, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest (>=7.0.1) A plugin to use approvaltests with pytest + :pypi:`pytest-approvaltests-geo` + *last release*: Feb 05, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest + + Extension for ApprovalTests.Python specific to geo data verification + + :pypi:`pytest-archon` + *last release*: Dec 18, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest >=7.2 + + Rule your architecture like a real developer + :pypi:`pytest-argus` *last release*: Jun 24, 2021, *status*: 5 - Production/Stable, @@ -1253,9 +1887,9 @@ This list contains 963 plugins. pyest results colection plugin :pypi:`pytest-arraydiff` - *last release*: Dec 06, 2018, + *last release*: Nov 27, 2023, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest >=4.6 pytest plugin to help with comparing array output from tests @@ -1266,6 +1900,13 @@ This list contains 963 plugins. Convenient ASGI client/server fixtures for Pytest + :pypi:`pytest-aspec` + *last release*: Dec 20, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A rspec format reporter for pytest + :pypi:`pytest-asptest` *last release*: Apr 28, 2018, *status*: 4 - Beta, @@ -1273,6 +1914,20 @@ This list contains 963 plugins. test Answer Set Programming programs + :pypi:`pytest-assertcount` + *last release*: Oct 23, 2022, + *status*: N/A, + *requires*: pytest (>=5.0.0) + + Plugin to count actual number of asserts in pytest + + :pypi:`pytest-assertions` + *last release*: Apr 27, 2022, + *status*: N/A, + *requires*: N/A + + Pytest Assertions + :pypi:`pytest-assertutil` *last release*: May 10, 2019, *status*: N/A, @@ -1281,7 +1936,7 @@ This list contains 963 plugins. pytest-assertutil :pypi:`pytest-assert-utils` - *last release*: Sep 21, 2021, + *last release*: Apr 14, 2022, *status*: 3 - Alpha, *requires*: N/A @@ -1294,6 +1949,13 @@ This list contains 963 plugins. A pytest plugin that allows multiple failures per test + :pypi:`pytest-assurka` + *last release*: Aug 04, 2022, + *status*: N/A, + *requires*: N/A + + A pytest plugin for Assurka Studio + :pypi:`pytest-ast-back-to-python` *last release*: Sep 29, 2019, *status*: 4 - Beta, @@ -1301,17 +1963,24 @@ This list contains 963 plugins. A plugin for pytest devs to view how assertion rewriting recodes the AST + :pypi:`pytest-asteroid` + *last release*: Aug 15, 2022, + *status*: N/A, + *requires*: pytest (>=6.2.5,<8.0.0) + + PyTest plugin for docker-based testing on database images + :pypi:`pytest-astropy` - *last release*: Sep 21, 2021, + *last release*: Sep 26, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.6) + *requires*: pytest >=4.6 Meta-package containing dependencies for testing :pypi:`pytest-astropy-header` - *last release*: Dec 18, 2019, + *last release*: Sep 06, 2022, *status*: 3 - Alpha, - *requires*: pytest (>=2.8) + *requires*: pytest (>=4.6) pytest plugin to add diagnostic information to the header of the test output @@ -1322,16 +1991,30 @@ This list contains 963 plugins. + :pypi:`pytest_async` + *last release*: Feb 26, 2020, + *status*: N/A, + *requires*: N/A + + pytest-async - Run your coroutine in event loop without decorator + + :pypi:`pytest-async-generators` + *last release*: Jul 05, 2023, + *status*: N/A, + *requires*: N/A + + Pytest fixtures for async generators + :pypi:`pytest-asyncio` - *last release*: Oct 15, 2021, + *last release*: Mar 19, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5.4.0) + *requires*: pytest <9,>=7.0.0 - Pytest support for asyncio. + Pytest support for asyncio :pypi:`pytest-asyncio-cooperative` - *last release*: Oct 12, 2021, - *status*: 4 - Beta, + *last release*: Feb 25, 2024, + *status*: N/A, *requires*: N/A Run all your asynchronous tests cooperatively. @@ -1357,6 +2040,13 @@ This list contains 963 plugins. Database testing fixtures using the SQLAlchemy asyncio API + :pypi:`pytest-atf-allure` + *last release*: Nov 29, 2023, + *status*: N/A, + *requires*: pytest (>=7.4.2,<8.0.0) + + 基于allure-pytest进行自定义 + :pypi:`pytest-atomic` *last release*: Nov 24, 2018, *status*: 4 - Beta, @@ -1378,6 +2068,13 @@ This list contains 963 plugins. Austin plugin for pytest + :pypi:`pytest-autocap` + *last release*: May 15, 2022, + *status*: N/A, + *requires*: pytest (<7.2,>=7.1.2) + + automatically capture test & fixture stdout/stderr to files + :pypi:`pytest-autochecklog` *last release*: Apr 25, 2015, *status*: 4 - Beta, @@ -1386,14 +2083,14 @@ This list contains 963 plugins. automatically check condition and log all the checks :pypi:`pytest-automation` - *last release*: Oct 01, 2021, + *last release*: May 20, 2022, *status*: N/A, - *requires*: pytest + *requires*: pytest (>=7.0.0) pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. :pypi:`pytest-automock` - *last release*: Apr 22, 2020, + *last release*: May 16, 2023, *status*: N/A, *requires*: pytest ; extra == 'dev' @@ -1413,6 +2110,13 @@ This list contains 963 plugins. This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. + :pypi:`pytest-aviator` + *last release*: Nov 04, 2022, + *status*: 4 - Beta, + *requires*: pytest + + Aviator's Flakybot pytest plugin that automatically reruns flaky tests. + :pypi:`pytest-avoidance` *last release*: May 23, 2019, *status*: 4 - Beta, @@ -1434,6 +2138,13 @@ This list contains 963 plugins. Protect your AWS credentials in unit tests + :pypi:`pytest-aws-fixtures` + *last release*: Feb 02, 2024, + *status*: N/A, + *requires*: pytest (>=8.0.0,<9.0.0) + + A series of fixtures to use in integration tests involving actual AWS services. + :pypi:`pytest-axe` *last release*: Nov 12, 2018, *status*: N/A, @@ -1441,11 +2152,32 @@ This list contains 963 plugins. pytest plugin for axe-selenium-python - :pypi:`pytest-azurepipelines` - *last release*: Jul 23, 2020, + :pypi:`pytest-axe-playwright-snapshot` + *last release*: Jul 25, 2023, + *status*: N/A, + *requires*: pytest + + A pytest plugin that runs Axe-core on Playwright pages and takes snapshots of the results. + + :pypi:`pytest-azure` + *last release*: Jan 18, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + Pytest utilities and mocks for Azure + + :pypi:`pytest-azure-devops` + *last release*: Jun 20, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) + Simplifies using azure devops parallel strategy (https://docs.microsoft.com/en-us/azure/devops/pipelines/test/parallel-testing-any-test-runner) with pytest. + + :pypi:`pytest-azurepipelines` + *last release*: Oct 06, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=5.0.0) + Formatting PyTest output for Azure Pipelines UI :pypi:`pytest-bandit` @@ -1455,20 +2187,48 @@ This list contains 963 plugins. A bandit plugin for pytest + :pypi:`pytest-bandit-xayon` + *last release*: Oct 17, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A bandit plugin for pytest + :pypi:`pytest-base-url` - *last release*: Jun 19, 2020, + *last release*: Jan 31, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.7.3) + *requires*: pytest>=7.0.0 pytest plugin for URL based testing :pypi:`pytest-bdd` - *last release*: Oct 25, 2021, + *last release*: Mar 17, 2024, *status*: 6 - Mature, - *requires*: pytest (>=4.3) + *requires*: pytest (>=6.2.0) BDD for pytest + :pypi:`pytest-bdd-html` + *last release*: Nov 22, 2022, + *status*: 3 - Alpha, + *requires*: pytest (!=6.0.0,>=5.0) + + pytest plugin to display BDD info in HTML test report + + :pypi:`pytest-bdd-ng` + *last release*: Dec 31, 2023, + *status*: 4 - Beta, + *requires*: pytest >=5.0 + + BDD for pytest + + :pypi:`pytest-bdd-report` + *last release*: Feb 19, 2024, + *status*: N/A, + *requires*: pytest >=7.1.3 + + A pytest-bdd plugin for generating useful and informative BDD test reports + :pypi:`pytest-bdd-splinter` *last release*: Aug 12, 2019, *status*: 5 - Production/Stable, @@ -1497,6 +2257,20 @@ This list contains 963 plugins. A pytest plugin that reports test results to the BeakerLib framework + :pypi:`pytest-beartype` + *last release*: Jan 25, 2024, + *status*: N/A, + *requires*: pytest + + Pytest plugin to run your tests with beartype checking enabled. + + :pypi:`pytest-bec-e2e` + *last release*: Apr 19, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + BEC pytest plugin for end-to-end tests + :pypi:`pytest-beds` *last release*: Jun 07, 2016, *status*: 4 - Beta, @@ -1504,6 +2278,13 @@ This list contains 963 plugins. Fixtures for testing Google Appengine (GAE) apps + :pypi:`pytest-beeprint` + *last release*: Jul 04, 2023, + *status*: 4 - Beta, + *requires*: N/A + + use icdiff for better error messages in pytest assertions + :pypi:`pytest-bench` *last release*: Jul 21, 2014, *status*: 3 - Alpha, @@ -1512,33 +2293,54 @@ This list contains 963 plugins. Benchmark utility that plugs into pytest. :pypi:`pytest-benchmark` - *last release*: Apr 17, 2021, + *last release*: Oct 25, 2022, *status*: 5 - Production/Stable, *requires*: pytest (>=3.8) A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. + :pypi:`pytest-better-datadir` + *last release*: Mar 13, 2023, + *status*: N/A, + *requires*: N/A + + A small example package + + :pypi:`pytest-better-parametrize` + *last release*: Mar 05, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + Better description of parametrized test cases + :pypi:`pytest-bg-process` - *last release*: Aug 17, 2021, + *last release*: Jan 24, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) Pytest plugin to initialize background process :pypi:`pytest-bigchaindb` - *last release*: Aug 17, 2021, + *last release*: Jan 24, 2022, *status*: 4 - Beta, *requires*: N/A A BigchainDB plugin for pytest. :pypi:`pytest-bigquery-mock` - *last release*: Aug 05, 2021, + *last release*: Dec 28, 2022, *status*: N/A, *requires*: pytest (>=5.0) Provides a mock fixture for python bigquery client + :pypi:`pytest-bisect-tests` + *last release*: Mar 25, 2024, + *status*: N/A, + *requires*: N/A + + Find tests leaking state and affecting other + :pypi:`pytest-black` *last release*: Oct 05, 2020, *status*: 4 - Beta, @@ -1553,6 +2355,13 @@ This list contains 963 plugins. Allow '--black' on older Pythons + :pypi:`pytest-black-ng` + *last release*: Oct 20, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + A pytest plugin to enable format checking with black + :pypi:`pytest-blame` *last release*: May 04, 2019, *status*: N/A, @@ -1561,9 +2370,9 @@ This list contains 963 plugins. A pytest plugin helps developers to debug by providing useful commits history. :pypi:`pytest-blender` - *last release*: Oct 29, 2021, + *last release*: Aug 10, 2023, *status*: N/A, - *requires*: pytest (==6.2.5) ; extra == 'dev' + *requires*: pytest ; extra == 'dev' Blender Pytest plugin. @@ -1575,7 +2384,7 @@ This list contains 963 plugins. Pytest plugin to emit notifications via the Blink(1) RGB LED :pypi:`pytest-blockage` - *last release*: Feb 13, 2019, + *last release*: Dec 21, 2021, *status*: N/A, *requires*: pytest @@ -1588,6 +2397,13 @@ This list contains 963 plugins. pytest plugin to mark a test as blocker and skip all other tests + :pypi:`pytest-blue` + *last release*: Sep 05, 2022, + *status*: N/A, + *requires*: N/A + + A pytest plugin that adds a \`blue\` fixture for printing stuff in blue. + :pypi:`pytest-board` *last release*: Jan 20, 2019, *status*: N/A, @@ -1595,6 +2411,20 @@ This list contains 963 plugins. Local continuous test runner with pytest and watchdog. + :pypi:`pytest-boost-xml` + *last release*: Nov 30, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Plugin for pytest to generate boost xml reports + + :pypi:`pytest-bootstrap` + *last release*: Mar 04, 2022, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-bpdb` *last release*: Jan 19, 2015, *status*: 2 - Pre-Alpha, @@ -1603,7 +2433,7 @@ This list contains 963 plugins. A py.test plug-in to enable drop to bpdb debugger on test failure. :pypi:`pytest-bravado` - *last release*: Jul 19, 2021, + *last release*: Feb 15, 2022, *status*: N/A, *requires*: N/A @@ -1630,6 +2460,13 @@ This list contains 963 plugins. A pytest plugin for running tests on a Briefcase project. + :pypi:`pytest-broadcaster` + *last release*: Apr 06, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + Pytest plugin to broadcast pytest output to various destinations + :pypi:`pytest-browser` *last release*: Dec 10, 2016, *status*: 3 - Alpha, @@ -1644,6 +2481,13 @@ This list contains 963 plugins. BrowserMob proxy plugin for py.test. + :pypi:`pytest_browserstack` + *last release*: Jan 27, 2016, + *status*: 4 - Beta, + *requires*: N/A + + Py.test plugin for BrowserStack + :pypi:`pytest-browserstack-local` *last release*: Feb 09, 2018, *status*: N/A, @@ -1651,15 +2495,22 @@ This list contains 963 plugins. \`\`py.test\`\` plugin to run \`\`BrowserStackLocal\`\` in background. + :pypi:`pytest-budosystems` + *last release*: May 07, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + Budo Systems is a martial arts school management system. This module is the Budo Systems Pytest Plugin. + :pypi:`pytest-bug` - *last release*: Jun 02, 2020, + *last release*: Sep 23, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.6.0) + *requires*: pytest >=7.1.0 Pytest plugin for marking tests as a bug :pypi:`pytest-bugtong-tag` - *last release*: Apr 23, 2021, + *last release*: Jan 16, 2022, *status*: N/A, *requires*: N/A @@ -1694,7 +2545,7 @@ This list contains 963 plugins. :pypi:`pytest-bwrap` - *last release*: Oct 26, 2018, + *last release*: Feb 25, 2024, *status*: 3 - Alpha, *requires*: N/A @@ -1708,9 +2559,9 @@ This list contains 963 plugins. pytest plugin with mechanisms for caching across test runs :pypi:`pytest-cache-assert` - *last release*: Nov 03, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=5) + *last release*: Aug 14, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=6.0.0) Cache assertion data to simplify regression testing of complex serializable data @@ -1721,6 +2572,20 @@ This list contains 963 plugins. Pytest plugin to only run tests affected by changes + :pypi:`pytest-cairo` + *last release*: Apr 17, 2022, + *status*: N/A, + *requires*: pytest + + Pytest support for cairo-lang and starknet + + :pypi:`pytest-call-checker` + *last release*: Oct 16, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=7.1.3,<8.0.0) + + Small pytest utility to easily create test doubles + :pypi:`pytest-camel-collect` *last release*: Aug 02, 2020, *status*: N/A, @@ -1749,15 +2614,15 @@ This list contains 963 plugins. pytest plugin to capture all deprecatedwarnings and put them in one file - :pypi:`pytest-capturelogs` - *last release*: Sep 11, 2021, - *status*: 3 - Alpha, - *requires*: N/A + :pypi:`pytest-capture-warnings` + *last release*: May 03, 2022, + *status*: N/A, + *requires*: pytest - A sample Python project + pytest plugin to capture all warnings and put them in one file of your choice :pypi:`pytest-cases` - *last release*: Nov 08, 2021, + *last release*: Apr 04, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -1785,11 +2650,18 @@ This list contains 963 plugins. Pytest plugin with server for catching HTTP requests. :pypi:`pytest-celery` - *last release*: May 06, 2021, + *last release*: Apr 11, 2024, + *status*: 4 - Beta, + *requires*: N/A + + Pytest plugin for Celery + + :pypi:`pytest-cfg-fetcher` + *last release*: Feb 26, 2024, *status*: N/A, *requires*: N/A - pytest-celery a shim pytest plugin to enable celery.contrib.pytest + Pass config options to your unit tests. :pypi:`pytest-chainmaker` *last release*: Oct 15, 2021, @@ -1805,6 +2677,20 @@ This list contains 963 plugins. A set of py.test fixtures for AWS Chalice + :pypi:`pytest-change-assert` + *last release*: Oct 19, 2022, + *status*: N/A, + *requires*: N/A + + 修改报错中文为英文 + + :pypi:`pytest-change-demo` + *last release*: Mar 02, 2022, + *status*: N/A, + *requires*: pytest + + turn . into √,turn F into x + :pypi:`pytest-change-report` *last release*: Sep 14, 2020, *status*: N/A, @@ -1812,6 +2698,13 @@ This list contains 963 plugins. turn . into √,turn F into x + :pypi:`pytest-change-xds` + *last release*: Apr 16, 2022, + *status*: N/A, + *requires*: pytest + + turn . into √,turn F into x + :pypi:`pytest-chdir` *last release*: Jan 28, 2020, *status*: N/A, @@ -1819,27 +2712,55 @@ This list contains 963 plugins. A pytest fixture for changing current working directory + :pypi:`pytest-check` + *last release*: Jan 18, 2024, + *status*: N/A, + *requires*: pytest>=7.0.0 + + A pytest plugin that allows multiple failures per test. + :pypi:`pytest-checkdocs` - *last release*: Jul 31, 2021, + *last release*: Mar 31, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.6) ; extra == 'testing' + *requires*: pytest>=6; extra == "testing" check the README when running tests :pypi:`pytest-checkipdb` - *last release*: Jul 22, 2020, + *last release*: Dec 04, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.9.2) + *requires*: pytest >=2.9.2 plugin to check if there are ipdb debugs left + :pypi:`pytest-check-library` + *last release*: Jul 17, 2022, + *status*: N/A, + *requires*: N/A + + check your missing library + + :pypi:`pytest-check-libs` + *last release*: Jul 17, 2022, + *status*: N/A, + *requires*: N/A + + check your missing library + :pypi:`pytest-check-links` *last release*: Jul 29, 2020, *status*: N/A, - *requires*: pytest (>=4.6) + *requires*: pytest<9,>=7.0 Check links in files + :pypi:`pytest-checklist` + *last release*: Mar 12, 2024, + *status*: N/A, + *requires*: N/A + + Pytest plugin to track and report unit/function coverage. + :pypi:`pytest-check-mk` *last release*: Nov 19, 2015, *status*: 4 - Beta, @@ -1847,6 +2768,48 @@ This list contains 963 plugins. pytest plugin to test Check_MK checks + :pypi:`pytest-check-requirements` + *last release*: Feb 20, 2024, + *status*: N/A, + *requires*: N/A + + A package to prevent Dependency Confusion attacks against Yandex. + + :pypi:`pytest-ch-framework` + *last release*: Apr 17, 2024, + *status*: N/A, + *requires*: pytest==8.0.1 + + My pytest framework + + :pypi:`pytest-chic-report` + *last release*: Jan 31, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + A pytest plugin to send a report and printing summary of tests. + + :pypi:`pytest-choose` + *last release*: Feb 04, 2024, + *status*: N/A, + *requires*: pytest >=7.0.0 + + Provide the pytest with the ability to collect use cases based on rules in text files + + :pypi:`pytest-chunks` + *last release*: Jul 05, 2022, + *status*: N/A, + *requires*: pytest (>=6.0.0) + + Run only a chunk of your test suite + + :pypi:`pytest_cid` + *last release*: Sep 01, 2023, + *status*: 4 - Beta, + *requires*: pytest >= 5.0, < 7.0 + + Compare data structures containing matching CIDs of different versions and encoding + :pypi:`pytest-circleci` *last release*: May 03, 2019, *status*: N/A, @@ -1855,12 +2818,19 @@ This list contains 963 plugins. py.test plugin for CircleCI :pypi:`pytest-circleci-parallelized` - *last release*: Mar 26, 2019, + *last release*: Oct 20, 2022, *status*: N/A, *requires*: N/A Parallelize pytest across CircleCI workers. + :pypi:`pytest-circleci-parallelized-rjp` + *last release*: Jun 21, 2022, + *status*: N/A, + *requires*: pytest + + Parallelize pytest across CircleCI workers. + :pypi:`pytest-ckan` *last release*: Apr 28, 2020, *status*: 4 - Beta, @@ -1876,21 +2846,49 @@ This list contains 963 plugins. A plugin providing an alternative, colourful diff output for failing assertions. :pypi:`pytest-cldf` - *last release*: May 06, 2019, + *last release*: Nov 07, 2022, *status*: N/A, - *requires*: N/A + *requires*: pytest (>=3.6) Easy quality control for CLDF datasets using pytest + :pypi:`pytest_cleanup` + *last release*: Jan 28, 2020, + *status*: N/A, + *requires*: N/A + + Automated, comprehensive and well-organised pytest test cases. + + :pypi:`pytest-cleanuptotal` + *last release*: Mar 19, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + A cleanup plugin for pytest + + :pypi:`pytest-clerk` + *last release*: Apr 19, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + A set of pytest fixtures to help with integration testing with Clerk. + :pypi:`pytest-click` - *last release*: Aug 29, 2020, + *last release*: Feb 11, 2022, *status*: 5 - Production/Stable, *requires*: pytest (>=5.0) - Py.test plugin for Click + Pytest plugin for Click + + :pypi:`pytest-cli-fixtures` + *last release*: Jul 28, 2022, + *status*: N/A, + *requires*: pytest (~=7.0) + + Automatically register fixtures for custom CLI arguments :pypi:`pytest-clld` - *last release*: Nov 29, 2021, + *last release*: Jul 06, 2022, *status*: N/A, *requires*: pytest (>=3.6) @@ -1910,6 +2908,27 @@ This list contains 963 plugins. pytest plugin for testing cloudflare workers + :pypi:`pytest-cloudist` + *last release*: Sep 02, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=7.1.2,<8.0.0) + + Distribute tests to cloud machines without fuss + + :pypi:`pytest-cmake` + *last release*: Mar 18, 2024, + *status*: N/A, + *requires*: pytest<9,>=4 + + Provide CMake module for Pytest + + :pypi:`pytest-cmake-presets` + *last release*: Dec 26, 2022, + *status*: N/A, + *requires*: pytest (>=7.2.0,<8.0.0) + + Execute CMake Presets via pytest + :pypi:`pytest-cobra` *last release*: Jun 29, 2019, *status*: 3 - Alpha, @@ -1917,13 +2936,20 @@ This list contains 963 plugins. PyTest plugin for testing Smart Contracts for Ethereum blockchain. - :pypi:`pytest-codeblocks` - *last release*: Oct 13, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=6) + :pypi:`pytest_codeblocks` + *last release*: Sep 17, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest >= 7.0.0 Test code blocks in your READMEs + :pypi:`pytest-codecarbon` + *last release*: Jun 15, 2022, + *status*: N/A, + *requires*: pytest + + Pytest plugin for measuring carbon emissions + :pypi:`pytest-codecheckers` *last release*: Feb 13, 2010, *status*: N/A, @@ -1932,7 +2958,7 @@ This list contains 963 plugins. pytest plugin to add source code sanity checks (pep8 and friends) :pypi:`pytest-codecov` - *last release*: Oct 27, 2021, + *last release*: Nov 29, 2022, *status*: 4 - Beta, *requires*: pytest (>=4.6.0) @@ -1945,6 +2971,13 @@ This list contains 963 plugins. Automatically create pytest test signatures + :pypi:`pytest-codeowners` + *last release*: Mar 30, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=6.0.0) + + Pytest plugin for selecting tests by GitHub CODEOWNERS. + :pypi:`pytest-codestyle` *last release*: Mar 23, 2020, *status*: 3 - Alpha, @@ -1952,6 +2985,20 @@ This list contains 963 plugins. pytest plugin to run pycodestyle + :pypi:`pytest-codspeed` + *last release*: Mar 19, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=3.8 + + Pytest plugin to create CodSpeed benchmarks + + :pypi:`pytest-collect-appoint-info` + *last release*: Aug 03, 2023, + *status*: N/A, + *requires*: pytest + + set your encoding + :pypi:`pytest-collect-formatter` *last release*: Mar 29, 2021, *status*: 5 - Production/Stable, @@ -1966,6 +3013,27 @@ This list contains 963 plugins. Formatter for pytest collect output + :pypi:`pytest-collect-interface-info-plugin` + *last release*: Sep 25, 2023, + *status*: 4 - Beta, + *requires*: N/A + + Get executed interface information in pytest interface automation framework + + :pypi:`pytest-collector` + *last release*: Aug 02, 2022, + *status*: N/A, + *requires*: pytest (>=7.0,<8.0) + + Python package for collecting pytest. + + :pypi:`pytest-collect-pytest-interinfo` + *last release*: Sep 26, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A simple plugin to use with pytest + :pypi:`pytest-colordots` *last release*: Oct 06, 2017, *status*: 5 - Production/Stable, @@ -1981,12 +3049,19 @@ This list contains 963 plugins. An interactive GUI test runner for PyTest :pypi:`pytest-common-subject` - *last release*: Nov 12, 2020, + *last release*: May 15, 2022, *status*: N/A, - *requires*: pytest (>=3.6,<7) + *requires*: pytest (>=3.6,<8) pytest framework for testing different aspects of a common method + :pypi:`pytest-compare` + *last release*: Jun 22, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + pytest plugin for comparing call arguments. + :pypi:`pytest-concurrent` *last release*: Jan 12, 2019, *status*: 4 - Beta, @@ -2002,16 +3077,16 @@ This list contains 963 plugins. Base configurations and utilities for developing your Python project test suite with pytest. :pypi:`pytest-confluence-report` - *last release*: Nov 06, 2020, + *last release*: Apr 17, 2022, *status*: N/A, *requires*: N/A Package stands for pytest plugin to upload results into Confluence page. :pypi:`pytest-console-scripts` - *last release*: Sep 28, 2021, + *last release*: May 31, 2023, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest (>=4.0.0) Pytest plugin for testing console scripts @@ -2023,9 +3098,9 @@ This list contains 963 plugins. pytest plugin with fixtures for testing consul aware apps :pypi:`pytest-container` - *last release*: Nov 19, 2021, - *status*: 3 - Alpha, - *requires*: pytest (>=3.10) + *last release*: Apr 10, 2024, + *status*: 4 - Beta, + *requires*: pytest>=3.10 Pytest fixtures for writing container based tests @@ -2044,12 +3119,26 @@ This list contains 963 plugins. A plugin to run tests written with the Contexts framework using pytest :pypi:`pytest-cookies` - *last release*: May 24, 2021, + *last release*: Mar 22, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.3.0) + *requires*: pytest (>=3.9.0) The pytest plugin for your Cookiecutter templates. 🍪 + :pypi:`pytest-copie` + *last release*: Jan 27, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + The pytest plugin for your copier templates 📒 + + :pypi:`pytest-copier` + *last release*: Dec 11, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.3.2 + + A pytest plugin to help testing Copier templates + :pypi:`pytest-couchdbkit` *last release*: Apr 17, 2012, *status*: N/A, @@ -2065,9 +3154,9 @@ This list contains 963 plugins. count erros and send email :pypi:`pytest-cov` - *last release*: Oct 04, 2021, + *last release*: Mar 24, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.6) + *requires*: pytest>=4.6 Pytest plugin for measuring coverage. @@ -2086,12 +3175,19 @@ This list contains 963 plugins. :pypi:`pytest-coverage-context` - *last release*: Jan 04, 2021, + *last release*: Jun 28, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.1.0) + *requires*: N/A Coverage dynamic context support for PyTest, including sub-processes + :pypi:`pytest-coveragemarkers` + *last release*: Apr 15, 2024, + *status*: N/A, + *requires*: pytest<8.0.0,>=7.1.2 + + Using pytest markers to track functional coverage and filtering of tests + :pypi:`pytest-cov-exclude` *last release*: Apr 29, 2016, *status*: 4 - Beta, @@ -2099,13 +3195,34 @@ This list contains 963 plugins. Pytest plugin for excluding tests based on coverage data + :pypi:`pytest_covid` + *last release*: Jun 24, 2020, + *status*: N/A, + *requires*: N/A + + Too many faillure, less tests. + :pypi:`pytest-cpp` - *last release*: Dec 03, 2021, + *last release*: Nov 01, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (!=5.4.0,!=5.4.1) + *requires*: pytest >=7.0 Use pytest's runner to discover and execute C++ tests + :pypi:`pytest-cppython` + *last release*: Mar 14, 2024, + *status*: N/A, + *requires*: N/A + + A pytest plugin that imports CPPython testing types + + :pypi:`pytest-cqase` + *last release*: Aug 22, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.2,<8.0.0) + + Custom qase pytest plugin + :pypi:`pytest-cram` *last release*: Aug 08, 2020, *status*: N/A, @@ -2120,6 +3237,20 @@ This list contains 963 plugins. Manages CrateDB instances during your integration tests + :pypi:`pytest-crayons` + *last release*: Oct 08, 2023, + *status*: N/A, + *requires*: pytest + + A pytest plugin for colorful print statements + + :pypi:`pytest-create` + *last release*: Feb 15, 2023, + *status*: 1 - Planning, + *requires*: N/A + + pytest-create + :pypi:`pytest-cricri` *last release*: Jan 27, 2018, *status*: N/A, @@ -2141,6 +3272,13 @@ This list contains 963 plugins. CSV output for pytest. + :pypi:`pytest-csv-params` + *last release*: Jul 01, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.4.0,<8.0.0) + + Pytest plugin for Test Case Parametrization with CSV files + :pypi:`pytest-curio` *last release*: Oct 07, 2020, *status*: N/A, @@ -2191,16 +3329,23 @@ This list contains 963 plugins. Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report :pypi:`pytest-cython` - *last release*: Jan 26, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=2.7.3) + *last release*: Apr 05, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=8 A plugin for testing Cython extension modules + :pypi:`pytest-cython-collect` + *last release*: Jun 17, 2022, + *status*: N/A, + *requires*: pytest + + + :pypi:`pytest-darker` - *last release*: Aug 16, 2020, + *last release*: Feb 25, 2024, *status*: N/A, - *requires*: pytest (>=6.0.1) ; extra == 'test' + *requires*: pytest <7,>=6.0.1 A pytest plugin for checking of modified code using Darker @@ -2211,6 +3356,13 @@ This list contains 963 plugins. pytest fixtures to run dash applications. + :pypi:`pytest-dashboard` + *last release*: Apr 18, 2024, + *status*: N/A, + *requires*: pytest<8.0.0,>=7.4.3 + + + :pypi:`pytest-data` *last release*: Nov 01, 2016, *status*: 5 - Production/Stable, @@ -2218,6 +3370,13 @@ This list contains 963 plugins. Useful functions for managing data for pytest fixtures + :pypi:`pytest-databases` + *last release*: Apr 19, 2024, + *status*: 4 - Beta, + *requires*: pytest + + Reusable database fixtures for any and all databases. + :pypi:`pytest-databricks` *last release*: Jul 29, 2020, *status*: N/A, @@ -2226,18 +3385,18 @@ This list contains 963 plugins. Pytest plugin for remote Databricks notebooks testing :pypi:`pytest-datadir` - *last release*: Oct 22, 2019, + *last release*: Oct 03, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.7.0) + *requires*: pytest >=5.0 pytest plugin for test data directories and files :pypi:`pytest-datadir-mgr` - *last release*: Aug 16, 2021, + *last release*: Apr 06, 2023, *status*: 5 - Production/Stable, - *requires*: pytest + *requires*: pytest (>=7.1) - Manager for test data providing downloads, caching of generated files, and a context for temp directories. + Manager for test data: downloads, artifact caching, and a tmpdir context. :pypi:`pytest-datadir-ng` *last release*: Dec 25, 2019, @@ -2246,6 +3405,20 @@ This list contains 963 plugins. Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. + :pypi:`pytest-datadir-nng` + *last release*: Nov 09, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.0.0,<8.0.0) + + Fixtures for pytest allowing test functions/methods to easily retrieve test resources from the local filesystem. + + :pypi:`pytest-data-extractor` + *last release*: Jul 19, 2022, + *status*: N/A, + *requires*: pytest (>=7.0.1) + + A pytest plugin to extract relevant metadata about tests into an external file (currently only json support) + :pypi:`pytest-data-file` *last release*: Dec 04, 2019, *status*: N/A, @@ -2254,11 +3427,11 @@ This list contains 963 plugins. Fixture "data" and "case_data" for test from yaml file :pypi:`pytest-datafiles` - *last release*: Oct 07, 2018, + *last release*: Feb 24, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=3.6) - py.test plugin to create a 'tmpdir' containing predefined files/directories. + py.test plugin to create a 'tmp_path' containing predefined files/directories. :pypi:`pytest-datafixtures` *last release*: Dec 05, 2020, @@ -2282,12 +3455,26 @@ This list contains 963 plugins. A pytest plugin for managing an archive of test data. :pypi:`pytest-datarecorder` - *last release*: Apr 20, 2020, + *last release*: Feb 15, 2024, *status*: 5 - Production/Stable, *requires*: pytest A py.test plugin recording and comparing test output. + :pypi:`pytest-dataset` + *last release*: Sep 01, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Plugin for loading different datasets for pytest by prefix from json or yaml files + + :pypi:`pytest-data-suites` + *last release*: Apr 06, 2024, + *status*: N/A, + *requires*: pytest<9.0,>=6.0 + + Class-based pytest parametrization + :pypi:`pytest-datatest` *last release*: Oct 15, 2020, *status*: 4 - Beta, @@ -2316,6 +3503,13 @@ This list contains 963 plugins. + :pypi:`pytest-dbt` + *last release*: Jun 08, 2023, + *status*: 2 - Pre-Alpha, + *requires*: pytest (>=7.0.0,<8.0.0) + + Unit test dbt models with standard python tooling + :pypi:`pytest-dbt-adapter` *last release*: Nov 24, 2021, *status*: N/A, @@ -2323,6 +3517,27 @@ This list contains 963 plugins. A pytest plugin for testing dbt adapter plugins + :pypi:`pytest-dbt-conventions` + *last release*: Mar 02, 2022, + *status*: N/A, + *requires*: pytest (>=6.2.5,<7.0.0) + + A pytest plugin for linting a dbt project's conventions + + :pypi:`pytest-dbt-core` + *last release*: Aug 25, 2023, + *status*: N/A, + *requires*: pytest >=6.2.5 ; extra == 'test' + + Pytest extension for dbt. + + :pypi:`pytest-dbt-postgres` + *last release*: Jan 02, 2024, + *status*: N/A, + *requires*: pytest (>=7.4.3,<8.0.0) + + Pytest tooling to unittest DBT & Postgres models + :pypi:`pytest-dbus-notification` *last release*: Mar 05, 2014, *status*: 5 - Production/Stable, @@ -2330,6 +3545,20 @@ This list contains 963 plugins. D-BUS notifications for pytest results. + :pypi:`pytest-dbx` + *last release*: Nov 29, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.3,<8.0.0) + + Pytest plugin to run unit tests for dbx (Databricks CLI extensions) related code + + :pypi:`pytest-dc` + *last release*: Aug 16, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest >=3.3 + + Manages Docker containers during your integration tests + :pypi:`pytest-deadfixtures` *last release*: Jul 23, 2020, *status*: 5 - Production/Stable, @@ -2337,6 +3566,13 @@ This list contains 963 plugins. A simple plugin to list unused fixtures in pytest + :pypi:`pytest-deduplicate` + *last release*: Aug 12, 2023, + *status*: 4 - Beta, + *requires*: pytest + + Identifies duplicate unit tests + :pypi:`pytest-deepcov` *last release*: Mar 30, 2021, *status*: N/A, @@ -2359,7 +3595,7 @@ This list contains 963 plugins. pytest示例插件 :pypi:`pytest-dependency` - *last release*: Feb 14, 2020, + *last release*: Dec 31, 2023, *status*: 4 - Beta, *requires*: N/A @@ -2380,9 +3616,9 @@ This list contains 963 plugins. Mark tests as testing a deprecated feature with a warning note. :pypi:`pytest-describe` - *last release*: Nov 13, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=4.0.0) + *last release*: Feb 10, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest <9,>=4.6 Describe-style plugin for pytest @@ -2393,6 +3629,13 @@ This list contains 963 plugins. plugin for rich text descriptions + :pypi:`pytest-deselect-if` + *last release*: Mar 24, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin to deselect pytests tests rather than using skipif + :pypi:`pytest-devpi-server` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -2400,6 +3643,13 @@ This list contains 963 plugins. DevPI server fixture for py.test + :pypi:`pytest-dhos` + *last release*: Sep 07, 2022, + *status*: N/A, + *requires*: N/A + + Common fixtures for pytest in DHOS services and libraries + :pypi:`pytest-diamond` *last release*: Aug 31, 2015, *status*: 4 - Beta, @@ -2428,6 +3678,34 @@ This list contains 963 plugins. A simple plugin to use with pytest + :pypi:`pytest-diffeo` + *last release*: Feb 20, 2024, + *status*: N/A, + *requires*: N/A + + A package to prevent Dependency Confusion attacks against Yandex. + + :pypi:`pytest-diff-selector` + *last release*: Feb 24, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=6.2.2) ; extra == 'all' + + Get tests affected by code changes (using git) + + :pypi:`pytest-difido` + *last release*: Oct 23, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=4.0.0) + + PyTest plugin for generating Difido reports + + :pypi:`pytest-dir-equal` + *last release*: Dec 11, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.3.2 + + pytest-dir-equals is a pytest plugin providing helpers to assert directories equality allowing golden testing + :pypi:`pytest-disable` *last release*: Sep 10, 2015, *status*: 4 - Beta, @@ -2443,16 +3721,23 @@ This list contains 963 plugins. Disable plugins per test :pypi:`pytest-discord` - *last release*: Mar 20, 2021, - *status*: 3 - Alpha, - *requires*: pytest (!=6.0.0,<7,>=3.3.2) + *last release*: Oct 18, 2023, + *status*: 4 - Beta, + *requires*: pytest !=6.0.0,<8,>=3.3.2 A pytest plugin to notify test results to a Discord channel. + :pypi:`pytest-discover` + *last release*: Mar 26, 2024, + *status*: N/A, + *requires*: pytest + + Pytest plugin to record discovered tests in a file + :pypi:`pytest-django` - *last release*: Dec 02, 2021, + *last release*: Jan 30, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.4.0) + *requires*: pytest >=7.0.0 A Django plugin for pytest. @@ -2464,9 +3749,9 @@ This list contains 963 plugins. A Django plugin for pytest. :pypi:`pytest-djangoapp` - *last release*: Aug 04, 2021, + *last release*: May 19, 2023, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest Nice pytest plugin to help you with Django pluggable application testing. @@ -2484,6 +3769,20 @@ This list contains 963 plugins. Integrate CasperJS with your django tests as a pytest fixture. + :pypi:`pytest-django-class` + *last release*: Aug 08, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A pytest plugin for running django in class-scoped fixtures + + :pypi:`pytest-django-docker-pg` + *last release*: Jan 30, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest <8.0.0,>=7.0.0 + + + :pypi:`pytest-django-dotenv` *last release*: Nov 26, 2019, *status*: 4 - Beta, @@ -2498,6 +3797,13 @@ This list contains 963 plugins. Factories for your Django models that can be used as Pytest fixtures. + :pypi:`pytest-django-filefield` + *last release*: May 09, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest >= 5.2 + + Replaces FileField.storage with something you can patch globally. + :pypi:`pytest-django-gcir` *last release*: Mar 06, 2018, *status*: 5 - Production/Stable, @@ -2513,8 +3819,8 @@ This list contains 963 plugins. Cleanup your Haystack indexes between tests :pypi:`pytest-django-ifactory` - *last release*: Jan 13, 2021, - *status*: 3 - Alpha, + *last release*: Aug 27, 2023, + *status*: 5 - Production/Stable, *requires*: N/A A model instance factory for pytest-django @@ -2527,7 +3833,7 @@ This list contains 963 plugins. The bare minimum to integrate py.test with Django. :pypi:`pytest-django-liveserver-ssl` - *last release*: Jul 30, 2021, + *last release*: Jan 20, 2022, *status*: 3 - Alpha, *requires*: N/A @@ -2576,8 +3882,8 @@ This list contains 963 plugins. py.test plugin for reporting the number of SQLs executed per django testcase. :pypi:`pytest-django-testing-postgresql` - *last release*: Dec 05, 2019, - *status*: 3 - Alpha, + *last release*: Jan 31, 2022, + *status*: 4 - Beta, *requires*: N/A Use a temporary PostgreSQL database with pytest-django @@ -2589,6 +3895,13 @@ This list contains 963 plugins. A documentation plugin for py.test. + :pypi:`pytest-docfiles` + *last release*: Dec 22, 2021, + *status*: 4 - Beta, + *requires*: pytest (>=3.7.0) + + pytest plugin to test codeblocks in your documentation. + :pypi:`pytest-docgen` *last release*: Apr 17, 2020, *status*: N/A, @@ -2597,11 +3910,18 @@ This list contains 963 plugins. An RST Documentation Generator for pytest-based test suites :pypi:`pytest-docker` - *last release*: Jun 14, 2021, + *last release*: Feb 02, 2024, *status*: N/A, - *requires*: pytest (<7.0,>=4.0) + *requires*: pytest <9.0,>=4.0 - Simple pytest fixtures for Docker and docker-compose based tests + Simple pytest fixtures for Docker and Docker Compose based tests + + :pypi:`pytest-docker-apache-fixtures` + *last release*: Feb 16, 2022, + *status*: 4 - Beta, + *requires*: pytest + + Pytest fixtures for testing with apache2 (httpd). :pypi:`pytest-docker-butla` *last release*: Jun 16, 2019, @@ -2624,6 +3944,13 @@ This list contains 963 plugins. Manages Docker containers during your integration tests + :pypi:`pytest-docker-compose-v2` + *last release*: Feb 28, 2024, + *status*: 4 - Beta, + *requires*: pytest<8,>=7.2.2 + + Manages Docker containers during your integration tests + :pypi:`pytest-docker-db` *last release*: Mar 20, 2021, *status*: 5 - Production/Stable, @@ -2632,19 +3959,26 @@ This list contains 963 plugins. A plugin to use docker databases for pytests :pypi:`pytest-docker-fixtures` - *last release*: Nov 23, 2021, + *last release*: Apr 03, 2024, *status*: 3 - Alpha, *requires*: N/A pytest docker fixtures :pypi:`pytest-docker-git-fixtures` - *last release*: Mar 11, 2021, + *last release*: Feb 09, 2022, *status*: 4 - Beta, *requires*: pytest Pytest fixtures for testing with git scm. + :pypi:`pytest-docker-haproxy-fixtures` + *last release*: Feb 09, 2022, + *status*: 4 - Beta, + *requires*: pytest + + Pytest fixtures for testing with haproxy. + :pypi:`pytest-docker-pexpect` *last release*: Jan 14, 2019, *status*: N/A, @@ -2667,16 +4001,30 @@ This list contains 963 plugins. Easy to use, simple to extend, pytest plugin that minimally leverages docker-py. :pypi:`pytest-docker-registry-fixtures` - *last release*: Mar 04, 2021, + *last release*: Apr 08, 2022, *status*: 4 - Beta, *requires*: pytest Pytest fixtures for testing with docker registries. + :pypi:`pytest-docker-service` + *last release*: Jan 03, 2024, + *status*: 3 - Alpha, + *requires*: pytest (>=7.1.3) + + pytest plugin to start docker container + + :pypi:`pytest-docker-squid-fixtures` + *last release*: Feb 09, 2022, + *status*: 4 - Beta, + *requires*: pytest + + Pytest fixtures for testing with squid. + :pypi:`pytest-docker-tools` - *last release*: Jul 23, 2021, + *last release*: Feb 17, 2022, *status*: 4 - Beta, - *requires*: pytest (>=6.0.1,<7.0.0) + *requires*: pytest (>=6.0.1) Docker integration tests for pytest @@ -2715,19 +4063,33 @@ This list contains 963 plugins. A simple pytest plugin to import names and add them to the doctest namespace. + :pypi:`pytest-doctest-mkdocstrings` + *last release*: Mar 02, 2024, + *status*: N/A, + *requires*: pytest + + Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) + :pypi:`pytest-doctestplus` - *last release*: Nov 16, 2021, - *status*: 3 - Alpha, - *requires*: pytest (>=4.6) + *last release*: Mar 10, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=4.6 Pytest plugin with advanced doctest features. - :pypi:`pytest-doctest-ufunc` - *last release*: Aug 02, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + :pypi:`pytest-dogu-report` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin for dogu report - A plugin to run doctests in docstrings of Numpy ufuncs + :pypi:`pytest-dogu-sdk` + *last release*: Dec 14, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin for the Dogu :pypi:`pytest-dolphin` *last release*: Nov 30, 2016, @@ -2736,6 +4098,13 @@ This list contains 963 plugins. Some extra stuff that we use ininternally + :pypi:`pytest-donde` + *last release*: Oct 01, 2023, + *status*: 4 - Beta, + *requires*: pytest >=7.3.1 + + record pytest session characteristics per test item (coverage and duration) into a persistent file and use them in your own plugin or script. + :pypi:`pytest-doorstop` *last release*: Jun 09, 2020, *status*: 4 - Beta, @@ -2750,10 +4119,24 @@ This list contains 963 plugins. A py.test plugin that parses environment files before running tests + :pypi:`pytest-dot-only-pkcopley` + *last release*: Oct 27, 2023, + *status*: N/A, + *requires*: N/A + + A Pytest marker for only running a single test + + :pypi:`pytest-draw` + *last release*: Mar 21, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + Pytest plugin for randomly selecting a specific number of tests + :pypi:`pytest-drf` - *last release*: Nov 12, 2020, + *last release*: Jul 12, 2022, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.6) + *requires*: pytest (>=3.7) A Django REST framework plugin for pytest. @@ -2765,14 +4148,21 @@ This list contains 963 plugins. Tool to allow webdriver automation to be ran locally or remotely :pypi:`pytest-drop-dup-tests` - *last release*: May 23, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=2.7) + *last release*: Mar 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=7 A Pytest plugin to drop duplicated tests during collection + :pypi:`pytest-dryrun` + *last release*: Jul 18, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.4.0,<8.0.0) + + A Pytest plugin to ignore tests during collection without reporting them in the test summary. + :pypi:`pytest-dummynet` - *last release*: Oct 13, 2021, + *last release*: Dec 15, 2021, *status*: 5 - Production/Stable, *requires*: pytest @@ -2792,6 +4182,13 @@ This list contains 963 plugins. + :pypi:`pytest-durations` + *last release*: Apr 22, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest (>=4.6) + + Pytest plugin reporting fixtures and test functions execution time. + :pypi:`pytest-dynamicrerun` *last release*: Aug 15, 2020, *status*: 4 - Beta, @@ -2800,7 +4197,7 @@ This list contains 963 plugins. A pytest plugin to rerun tests dynamically based off of test outcome and output. :pypi:`pytest-dynamodb` - *last release*: Jun 03, 2021, + *last release*: Mar 12, 2024, *status*: 5 - Production/Stable, *requires*: pytest @@ -2814,11 +4211,11 @@ This list contains 963 plugins. pytest-easy-addoption: Easy way to work with pytest addoption :pypi:`pytest-easy-api` - *last release*: Mar 26, 2018, + *last release*: Feb 16, 2024, *status*: N/A, *requires*: N/A - Simple API testing with pytest + A package to prevent Dependency Confusion attacks against Yandex. :pypi:`pytest-easyMPI` *last release*: Oct 21, 2020, @@ -2841,6 +4238,13 @@ This list contains 963 plugins. Pytest plugin for easy testing against servers + :pypi:`pytest-ebics-sandbox` + *last release*: Aug 15, 2022, + *status*: N/A, + *requires*: N/A + + A pytest plugin for testing against an EBICS sandbox server. Requires docker. + :pypi:`pytest-ec2` *last release*: Oct 22, 2019, *status*: 3 - Alpha, @@ -2849,16 +4253,23 @@ This list contains 963 plugins. Pytest execution on EC2 instance :pypi:`pytest-echo` - *last release*: Jan 08, 2020, + *last release*: Dec 05, 2023, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest >=2.2 pytest plugin with mechanisms for echoing environment variables, package version and generic attributes + :pypi:`pytest-ekstazi` + *last release*: Sep 10, 2022, + *status*: N/A, + *requires*: pytest + + Pytest plugin to select test using Ekstazi algorithm + :pypi:`pytest-elasticsearch` - *last release*: May 12, 2021, + *last release*: Mar 15, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest >=7.0 Elasticsearch fixtures and fixture factories for Pytest. @@ -2869,10 +4280,17 @@ This list contains 963 plugins. Tool to help automate user interfaces + :pypi:`pytest-eliot` + *last release*: Aug 31, 2022, + *status*: 1 - Planning, + *requires*: pytest (>=5.4.0) + + An eliot plugin for pytest. + :pypi:`pytest-elk-reporter` - *last release*: Jan 24, 2021, + *last release*: Apr 04, 2024, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest>=3.5.0 A simple plugin to use with pytest @@ -2884,53 +4302,67 @@ This list contains 963 plugins. Send execution result email :pypi:`pytest-embedded` - *last release*: Nov 29, 2021, - *status*: N/A, - *requires*: pytest (>=6.2.0) + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.0 + + A pytest plugin that designed for embedded testing. + + :pypi:`pytest-embedded-arduino` + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A - pytest embedded plugin + Make pytest-embedded plugin work with Arduino. :pypi:`pytest-embedded-idf` - *last release*: Nov 29, 2021, - *status*: N/A, + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for esp-idf project + Make pytest-embedded plugin work with ESP-IDF. :pypi:`pytest-embedded-jtag` - *last release*: Nov 29, 2021, - *status*: N/A, + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing with jtag + Make pytest-embedded plugin work with JTAG. :pypi:`pytest-embedded-qemu` - *last release*: Nov 29, 2021, - *status*: N/A, + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for qemu, not target chip + Make pytest-embedded plugin work with QEMU. - :pypi:`pytest-embedded-qemu-idf` - *last release*: Jun 29, 2021, - *status*: N/A, + :pypi:`pytest-embedded-serial` + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for esp-idf project by qemu, not target chip + Make pytest-embedded plugin work with Serial. - :pypi:`pytest-embedded-serial` - *last release*: Nov 29, 2021, - *status*: N/A, + :pypi:`pytest-embedded-serial-esp` + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing serial ports + Make pytest-embedded plugin work with Espressif target boards. - :pypi:`pytest-embedded-serial-esp` - *last release*: Nov 29, 2021, - *status*: N/A, + :pypi:`pytest-embedded-wokwi` + *last release*: Apr 09, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing espressif boards via serial ports + Make pytest-embedded plugin work with the Wokwi CLI. + + :pypi:`pytest-embrace` + *last release*: Mar 25, 2023, + *status*: N/A, + *requires*: pytest (>=7.0,<8.0) + + 💝 Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. :pypi:`pytest-emoji` *last release*: Feb 19, 2019, @@ -2940,16 +4372,16 @@ This list contains 963 plugins. A pytest plugin that adds emojis to your test result report :pypi:`pytest-emoji-output` - *last release*: Oct 10, 2021, + *last release*: Apr 09, 2023, *status*: 4 - Beta, - *requires*: pytest (==6.0.1) + *requires*: pytest (==7.0.1) Pytest plugin to represent test output with emoji support :pypi:`pytest-enabler` - *last release*: Nov 08, 2021, + *last release*: Mar 21, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=6) ; extra == 'testing' + *requires*: pytest>=6; extra == "testing" Enable installed pytest plugins @@ -2967,6 +4399,27 @@ This list contains 963 plugins. set your encoding and logger + :pypi:`pytest-encoding` + *last release*: Aug 11, 2023, + *status*: N/A, + *requires*: pytest + + set your encoding and logger + + :pypi:`pytest_energy_reporter` + *last release*: Mar 28, 2024, + *status*: 3 - Alpha, + *requires*: pytest<9.0.0,>=8.1.1 + + An energy estimation reporter for pytest + + :pypi:`pytest-enhanced-reports` + *last release*: Dec 15, 2022, + *status*: N/A, + *requires*: N/A + + Enhanced test reports for pytest + :pypi:`pytest-enhancements` *last release*: Oct 30, 2019, *status*: 4 - Beta, @@ -2975,11 +4428,11 @@ This list contains 963 plugins. Improvements for pytest (rejected upstream) :pypi:`pytest-env` - *last release*: Jun 16, 2017, - *status*: 4 - Beta, - *requires*: N/A + *last release*: Nov 28, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.4.3 - py.test plugin that allows you to add environment variables. + pytest plugin that allows you to add environment variables. :pypi:`pytest-envfiles` *last release*: Oct 08, 2015, @@ -2995,6 +4448,13 @@ This list contains 963 plugins. Push information about the running pytest into envvars + :pypi:`pytest-environment` + *last release*: Mar 17, 2024, + *status*: 1 - Planning, + *requires*: N/A + + Pytest Environment + :pypi:`pytest-envraw` *last release*: Aug 27, 2020, *status*: 4 - Beta, @@ -3023,6 +4483,13 @@ This list contains 963 plugins. pytest plugin to check for commented out code + :pypi:`pytest_erp` + *last release*: Jan 13, 2015, + *status*: N/A, + *requires*: N/A + + py.test plugin to send test info to report portal dynamically + :pypi:`pytest-error-for-skips` *last release*: Dec 19, 2019, *status*: 4 - Beta, @@ -3045,7 +4512,7 @@ This list contains 963 plugins. pytest-ethereum: Pytest library for ethereum projects. :pypi:`pytest-eucalyptus` - *last release*: Aug 13, 2019, + *last release*: Jun 28, 2022, *status*: N/A, *requires*: pytest (>=4.2.0) @@ -3058,8 +4525,36 @@ This list contains 963 plugins. Applies eventlet monkey-patch as a pytest plugin. + :pypi:`pytest-evm` + *last release*: Apr 20, 2024, + *status*: 4 - Beta, + *requires*: pytest<9.0.0,>=8.1.1 + + The testing package containing tools to test Web3-based projects + + :pypi:`pytest_exact_fixtures` + *last release*: Feb 04, 2019, + *status*: N/A, + *requires*: N/A + + Parse queries in Lucene and Elasticsearch syntaxes + + :pypi:`pytest-examples` + *last release*: Jul 11, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7 + + Pytest plugin for testing examples in docstrings and markdown files. + + :pypi:`pytest-exasol-itde` + *last release*: Feb 15, 2024, + *status*: N/A, + *requires*: pytest (>=7,<9) + + + :pypi:`pytest-excel` - *last release*: Oct 06, 2020, + *last release*: Sep 14, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -3080,12 +4575,26 @@ This list contains 963 plugins. Walk your code through exception script to check it's resiliency to failures. :pypi:`pytest-executable` - *last release*: Nov 10, 2021, - *status*: 4 - Beta, - *requires*: pytest (<6.3,>=4.3) + *last release*: Oct 07, 2023, + *status*: N/A, + *requires*: pytest <8,>=5 pytest plugin for testing executables + :pypi:`pytest-execution-timer` + *last release*: Dec 24, 2021, + *status*: 4 - Beta, + *requires*: N/A + + A timer for the phases of Pytest's execution. + + :pypi:`pytest-exit-code` + *last release*: Feb 23, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + A pytest plugin that overrides the built-in exit codes to retain more information about the test results. + :pypi:`pytest-expect` *last release*: Apr 21, 2016, *status*: 4 - Beta, @@ -3093,8 +4602,15 @@ This list contains 963 plugins. py.test plugin to store test expectations and mark tests based on them + :pypi:`pytest-expectdir` + *last release*: Mar 19, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=5.0) + + A pytest plugin to provide initial/expected directories, and check a test transforms the initial directory to the expected one + :pypi:`pytest-expecter` - *last release*: Jul 08, 2020, + *last release*: Sep 18, 2022, *status*: 5 - Production/Stable, *requires*: N/A @@ -3107,6 +4623,20 @@ This list contains 963 plugins. This plugin is used to expect multiple assert using pytest framework. + :pypi:`pytest-expect-test` + *last release*: Apr 10, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A fixture to support expect tests in pytest + + :pypi:`pytest-experiments` + *last release*: Dec 13, 2021, + *status*: 4 - Beta, + *requires*: pytest (>=6.2.5,<7.0.0) + + A pytest plugin to help developers of research-oriented software projects keep track of the results of their numerical experiments. + :pypi:`pytest-explicit` *last release*: Jun 15, 2021, *status*: 5 - Production/Stable, @@ -3115,12 +4645,33 @@ This list contains 963 plugins. A Pytest plugin to ignore certain marked tests by default :pypi:`pytest-exploratory` - *last release*: Aug 03, 2021, + *last release*: Aug 18, 2023, *status*: N/A, - *requires*: pytest (>=5.3) + *requires*: pytest (>=6.2) Interactive console for pytest. + :pypi:`pytest-explorer` + *last release*: Aug 01, 2023, + *status*: N/A, + *requires*: N/A + + terminal ui for exploring and running tests + + :pypi:`pytest-ext` + *last release*: Mar 31, 2024, + *status*: N/A, + *requires*: pytest>=5.3 + + pytest plugin for automation test + + :pypi:`pytest-extensions` + *last release*: Aug 17, 2022, + *status*: 4 - Beta, + *requires*: pytest ; extra == 'testing' + + A collection of helpers for pytest to ease testing + :pypi:`pytest-external-blockers` *last release*: Oct 05, 2021, *status*: N/A, @@ -3128,6 +4679,13 @@ This list contains 963 plugins. a special outcome for tests that are blocked for external reasons + :pypi:`pytest_extra` + *last release*: Aug 14, 2014, + *status*: N/A, + *requires*: N/A + + Some helpers for writing tests with pytest. + :pypi:`pytest-extra-durations` *last release*: Apr 21, 2020, *status*: 4 - Beta, @@ -3135,6 +4693,13 @@ This list contains 963 plugins. A pytest plugin to get durations on a per-function basis and per module basis. + :pypi:`pytest-extra-markers` + *last release*: Mar 05, 2023, + *status*: 4 - Beta, + *requires*: pytest + + Additional pytest markers to dynamically enable/disable tests viia CLI flags + :pypi:`pytest-fabric` *last release*: Sep 12, 2018, *status*: 5 - Production/Stable, @@ -3142,6 +4707,13 @@ This list contains 963 plugins. Provides test utilities to run fabric task tests by using docker containers + :pypi:`pytest-factor` + *last release*: Feb 20, 2024, + *status*: N/A, + *requires*: N/A + + A package to prevent Dependency Confusion attacks against Yandex. + :pypi:`pytest-factory` *last release*: Sep 06, 2020, *status*: 3 - Alpha, @@ -3150,9 +4722,9 @@ This list contains 963 plugins. Use factories for test setup with py.test :pypi:`pytest-factoryboy` - *last release*: Dec 30, 2020, + *last release*: Mar 05, 2024, *status*: 6 - Mature, - *requires*: pytest (>=4.6) + *requires*: pytest (>=6.2) Factory Boy support for pytest. @@ -3164,12 +4736,19 @@ This list contains 963 plugins. Generates pytest fixtures that allow the use of type hinting :pypi:`pytest-factoryboy-state` - *last release*: Dec 11, 2020, - *status*: 4 - Beta, + *last release*: Mar 22, 2022, + *status*: 5 - Production/Stable, *requires*: pytest (>=5.0) Simple factoryboy random state management + :pypi:`pytest-failed-screen-record` + *last release*: Jan 05, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.1.2d,<8.0.0) + + Create a video of the screen when pytest fails + :pypi:`pytest-failed-screenshot` *last release*: Apr 21, 2021, *status*: N/A, @@ -3184,6 +4763,13 @@ This list contains 963 plugins. A pytest plugin that helps better distinguishing real test failures from setup flakiness. + :pypi:`pytest-fail-slow` + *last release*: Feb 11, 2024, + *status*: N/A, + *requires*: pytest>=7.0 + + Fail tests that take too long to run + :pypi:`pytest-faker` *last release*: Dec 19, 2016, *status*: 6 - Mature, @@ -3199,11 +4785,11 @@ This list contains 963 plugins. Pytest helpers for Falcon. :pypi:`pytest-falcon-client` - *last release*: Mar 19, 2019, + *last release*: Feb 21, 2024, *status*: N/A, *requires*: N/A - Pytest \`client\` fixture for the Falcon Framework + A package to prevent Dependency Confusion attacks against Yandex. :pypi:`pytest-fantasy` *last release*: Mar 14, 2019, @@ -3219,15 +4805,22 @@ This list contains 963 plugins. + :pypi:`pytest-fastapi-deps` + *last release*: Jul 20, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + A fixture which allows easy replacement of fastapi dependencies for testing + :pypi:`pytest-fastest` - *last release*: Mar 05, 2020, - *status*: N/A, - *requires*: N/A + *last release*: Oct 04, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=4.4) Use SCM and coverage to run only needed tests :pypi:`pytest-fast-first` - *last release*: Apr 02, 2021, + *last release*: Jan 19, 2023, *status*: 3 - Alpha, *requires*: pytest @@ -3254,6 +4847,13 @@ This list contains 963 plugins. py.test figleaf coverage plugin + :pypi:`pytest-file` + *last release*: Mar 18, 2024, + *status*: 1 - Planning, + *requires*: N/A + + Pytest File + :pypi:`pytest-filecov` *last release*: Jun 27, 2021, *status*: 4 - Beta, @@ -3275,6 +4875,13 @@ This list contains 963 plugins. A pytest plugin that runs marked tests when files change. + :pypi:`pytest-file-watcher` + *last release*: Mar 23, 2023, + *status*: N/A, + *requires*: pytest + + Pytest-File-Watcher is a CLI tool that watches for changes in your code and runs pytest on the changed files. + :pypi:`pytest-filter-case` *last release*: Nov 05, 2020, *status*: N/A, @@ -3283,16 +4890,16 @@ This list contains 963 plugins. run test cases filter by mark :pypi:`pytest-filter-subpackage` - *last release*: Jan 09, 2020, - *status*: 3 - Alpha, - *requires*: pytest (>=3.0) + *last release*: Mar 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=4.6 Pytest plugin for filtering based on sub-packages :pypi:`pytest-find-dependencies` - *last release*: Apr 21, 2021, + *last release*: Mar 16, 2024, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest >=4.3.0 A pytest plugin to find dependencies between tests @@ -3310,6 +4917,20 @@ This list contains 963 plugins. pytest plugin to manipulate firefox + :pypi:`pytest-fixture-classes` + *last release*: Sep 02, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest + + Fixtures as classes that work well with dependency injection, autocompletetion, type checkers, and language servers + + :pypi:`pytest-fixturecollection` + *last release*: Feb 22, 2024, + *status*: 4 - Beta, + *requires*: pytest >=3.5.0 + + A pytest plugin to collect tests based on fixtures being used by tests + :pypi:`pytest-fixture-config` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -3332,12 +4953,33 @@ This list contains 963 plugins. A pytest plugin to add markers based on fixtures used. :pypi:`pytest-fixture-order` - *last release*: Aug 25, 2020, - *status*: N/A, + *last release*: May 16, 2022, + *status*: 5 - Production/Stable, *requires*: pytest (>=3.0) pytest plugin to control fixture evaluation order + :pypi:`pytest-fixture-ref` + *last release*: Nov 17, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Lets users reference fixtures without name matching magic. + + :pypi:`pytest-fixture-remover` + *last release*: Feb 14, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + A LibCST codemod to remove pytest fixtures applied via the usefixtures decorator, as well as its parametrizations. + + :pypi:`pytest-fixture-rtttg` + *last release*: Feb 23, 2022, + *status*: N/A, + *requires*: pytest (>=7.0.1,<8.0.0) + + Warn or fail on fixture name clash + :pypi:`pytest-fixtures` *last release*: May 01, 2019, *status*: 5 - Production/Stable, @@ -3360,21 +5002,28 @@ This list contains 963 plugins. A pytest plugin to assert type annotations at runtime. :pypi:`pytest-flake8` - *last release*: Dec 16, 2020, + *last release*: Mar 18, 2022, *status*: 4 - Beta, - *requires*: pytest (>=3.5) + *requires*: pytest (>=7.0) pytest plugin to check FLAKE8 requirements :pypi:`pytest-flake8-path` - *last release*: Aug 11, 2021, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest A pytest fixture for testing flake8 plugins. + :pypi:`pytest-flake8-v2` + *last release*: Mar 01, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.0) + + pytest plugin to check FLAKE8 requirements + :pypi:`pytest-flakefinder` - *last release*: Jul 28, 2020, + *last release*: Oct 26, 2022, *status*: 4 - Beta, *requires*: pytest (>=2.7.1) @@ -3395,14 +5044,21 @@ This list contains 963 plugins. Flaptastic py.test plugin :pypi:`pytest-flask` - *last release*: Feb 27, 2021, + *last release*: Oct 23, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.2) + *requires*: pytest >=5.2 A set of py.test fixtures to test Flask applications. + :pypi:`pytest-flask-ligand` + *last release*: Apr 25, 2023, + *status*: 4 - Beta, + *requires*: pytest (~=7.3) + + Pytest fixtures and helper functions to use for testing flask-ligand microservices. + :pypi:`pytest-flask-sqlalchemy` - *last release*: Apr 04, 2019, + *last release*: Apr 30, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.2.1) @@ -3415,6 +5071,34 @@ This list contains 963 plugins. Run tests in transactions using pytest, Flask, and SQLalchemy. + :pypi:`pytest-flexreport` + *last release*: Apr 15, 2023, + *status*: 4 - Beta, + *requires*: pytest + + + + :pypi:`pytest-fluent` + *last release*: Jun 26, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + A pytest plugin in order to provide logs via fluentd + + :pypi:`pytest-fluentbit` + *last release*: Jun 16, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + A pytest plugin in order to provide logs via fluentbit + + :pypi:`pytest-fly` + *last release*: Apr 14, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + pytest observer + :pypi:`pytest-flyte` *last release*: May 03, 2021, *status*: N/A, @@ -3429,6 +5113,13 @@ This list contains 963 plugins. A pytest plugin that alerts user of failed test cases with screen notifications + :pypi:`pytest-forbid` + *last release*: Mar 07, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.2,<8.0.0) + + + :pypi:`pytest-forcefail` *last release*: May 15, 2018, *status*: 4 - Beta, @@ -3436,6 +5127,13 @@ This list contains 963 plugins. py.test plugin to make the test failing regardless of pytest.mark.xfail + :pypi:`pytest-forks` + *last release*: Mar 05, 2024, + *status*: N/A, + *requires*: N/A + + Fork helper for pytest + :pypi:`pytest-forward-compatability` *last release*: Sep 06, 2020, *status*: N/A, @@ -3450,6 +5148,13 @@ This list contains 963 plugins. A pytest plugin to shim pytest commandline options for fowards compatibility + :pypi:`pytest-frappe` + *last release*: Oct 29, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.0.0 + + Pytest Frappe Plugin - A set of pytest fixtures to test Frappe applications + :pypi:`pytest-freezegun` *last release*: Jul 19, 2020, *status*: 4 - Beta, @@ -3457,6 +5162,13 @@ This list contains 963 plugins. Wrap tests with fixtures in freeze_time + :pypi:`pytest-freezer` + *last release*: Jun 21, 2023, + *status*: N/A, + *requires*: pytest >= 3.6 + + Pytest plugin providing a fixture interface for spulec/freezegun + :pypi:`pytest-freeze-reqs` *last release*: Apr 29, 2021, *status*: N/A, @@ -3465,7 +5177,7 @@ This list contains 963 plugins. Check if requirement files are frozen :pypi:`pytest-frozen-uuids` - *last release*: Oct 19, 2021, + *last release*: Apr 17, 2022, *status*: N/A, *requires*: pytest (>=3.0) @@ -3499,6 +5211,27 @@ This list contains 963 plugins. + :pypi:`pytest-fzf` + *last release*: Feb 07, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.0.0 + + fzf-based test selector for pytest + + :pypi:`pytest_gae` + *last release*: Aug 03, 2016, + *status*: 3 - Alpha, + *requires*: N/A + + pytest plugin for apps written with Google's AppEngine + + :pypi:`pytest-gather-fixtures` + *last release*: Apr 12, 2022, + *status*: N/A, + *requires*: pytest (>=6.0.0) + + set up asynchronous pytest fixtures concurrently + :pypi:`pytest-gc` *last release*: Feb 01, 2018, *status*: N/A, @@ -3513,6 +5246,20 @@ This list contains 963 plugins. Uses gcov to measure test coverage of a C library + :pypi:`pytest-gcs` + *last release*: Mar 01, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=6.2 + + GCS fixtures and fixture factories for Pytest. + + :pypi:`pytest-gee` + *last release*: Feb 15, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + The Python plugin for your GEE based packages. + :pypi:`pytest-gevent` *last release*: Feb 25, 2020, *status*: N/A, @@ -3527,6 +5274,13 @@ This list contains 963 plugins. A flexible framework for executing BDD gherkin tests + :pypi:`pytest-gh-log-group` + *last release*: Jan 11, 2022, + *status*: 3 - Alpha, + *requires*: pytest + + pytest plugin for gh actions + :pypi:`pytest-ghostinspector` *last release*: May 17, 2016, *status*: 3 - Alpha, @@ -3535,9 +5289,9 @@ This list contains 963 plugins. For finding/executing Ghost Inspector tests :pypi:`pytest-girder` - *last release*: Nov 30, 2021, + *last release*: Apr 12, 2024, *status*: N/A, - *requires*: N/A + *requires*: pytest>=3.6 A set of pytest fixtures for testing Girder applications. @@ -3548,6 +5302,13 @@ This list contains 963 plugins. Git repository fixture for py.test + :pypi:`pytest-gitconfig` + *last release*: Oct 15, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.1.2 + + Provide a gitconfig sandbox for testing + :pypi:`pytest-gitcov` *last release*: Jan 11, 2020, *status*: 2 - Pre-Alpha, @@ -3555,6 +5316,13 @@ This list contains 963 plugins. Pytest plugin for reporting on coverage of the last git commit. + :pypi:`pytest-git-diff` + *last release*: Apr 02, 2024, + *status*: N/A, + *requires*: N/A + + Pytest plugin that allows the user to select the tests affected by a range of git commits + :pypi:`pytest-git-fixtures` *last release*: Mar 11, 2021, *status*: 4 - Beta, @@ -3570,12 +5338,19 @@ This list contains 963 plugins. Plugin for py.test that associates tests with github issues using a marker. :pypi:`pytest-github-actions-annotate-failures` - *last release*: Oct 24, 2021, - *status*: N/A, + *last release*: May 04, 2023, + *status*: 5 - Production/Stable, *requires*: pytest (>=4.0.0) pytest plugin to annotate failed tests with a workflow command for GitHub Actions + :pypi:`pytest-github-report` + *last release*: Jun 03, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Generate a GitHub report using pytest in GitHub Workflows + :pypi:`pytest-gitignore` *last release*: Jul 17, 2015, *status*: 4 - Beta, @@ -3583,8 +5358,36 @@ This list contains 963 plugins. py.test plugin to ignore the same files as git + :pypi:`pytest-gitlabci-parallelized` + *last release*: Mar 08, 2023, + *status*: N/A, + *requires*: N/A + + Parallelize pytest across GitLab CI workers. + + :pypi:`pytest-gitlab-code-quality` + *last release*: Apr 03, 2024, + *status*: N/A, + *requires*: pytest>=8.1.1 + + Collects warnings while testing and generates a GitLab Code Quality Report. + + :pypi:`pytest-gitlab-fold` + *last release*: Dec 31, 2023, + *status*: 4 - Beta, + *requires*: pytest >=2.6.0 + + Folds output sections in GitLab CI build log + + :pypi:`pytest-git-selector` + *last release*: Nov 17, 2022, + *status*: N/A, + *requires*: N/A + + Utility to select tests that have had its dependencies modified (as identified by git diff) + :pypi:`pytest-glamor-allure` - *last release*: Nov 26, 2021, + *last release*: Jul 22, 2022, *status*: 4 - Beta, *requires*: pytest @@ -3598,12 +5401,26 @@ This list contains 963 plugins. Pytest fixtures for testing with gnupg. :pypi:`pytest-golden` - *last release*: Nov 23, 2020, + *last release*: Jul 18, 2022, *status*: N/A, - *requires*: pytest (>=6.1.2,<7.0.0) + *requires*: pytest (>=6.1.2) Plugin for pytest that offloads expected outputs to data files + :pypi:`pytest-goldie` + *last release*: May 23, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A plugin to support golden tests with pytest. + + :pypi:`pytest-google-chat` + *last release*: Mar 27, 2022, + *status*: 4 - Beta, + *requires*: pytest + + Notify google chat channel for test results + :pypi:`pytest-graphql-schema` *last release*: Oct 18, 2019, *status*: N/A, @@ -3618,6 +5435,13 @@ This list contains 963 plugins. Green progress dots + :pypi:`pytest-group-by-class` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=2.5) + + A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. + :pypi:`pytest-growl` *last release*: Jan 13, 2014, *status*: 5 - Production/Stable, @@ -3632,6 +5456,20 @@ This list contains 963 plugins. pytest plugin for grpc + :pypi:`pytest-grunnur` + *last release*: Feb 05, 2023, + *status*: N/A, + *requires*: N/A + + Py.Test plugin for Grunnur-based packages. + + :pypi:`pytest_gui_status` + *last release*: Jan 23, 2016, + *status*: N/A, + *requires*: pytest + + Show pytest status in gui + :pypi:`pytest-hammertime` *last release*: Jul 28, 2018, *status*: N/A, @@ -3639,8 +5477,22 @@ This list contains 963 plugins. Display "🔨 " instead of "." for passed pytest tests. + :pypi:`pytest-hardware-test-report` + *last release*: Apr 01, 2024, + *status*: 4 - Beta, + *requires*: pytest<9.0.0,>=8.0.0 + + A simple plugin to use with pytest + + :pypi:`pytest-harmony` + *last release*: Jan 17, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.1,<8.0.0) + + Chain tests and data with pytest + :pypi:`pytest-harvest` - *last release*: Apr 01, 2021, + *last release*: Mar 16, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -3654,12 +5506,19 @@ This list contains 963 plugins. A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. :pypi:`pytest-helm-charts` - *last release*: Oct 26, 2021, + *last release*: Feb 07, 2024, *status*: 4 - Beta, - *requires*: pytest (>=6.1.2,<7.0.0) + *requires*: pytest (>=8.0.0,<9.0.0) A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. + :pypi:`pytest-helm-templates` + *last release*: Apr 05, 2024, + *status*: N/A, + *requires*: pytest~=7.4.0; extra == "dev" + + Pytest fixtures for unit testing the output of helm templates + :pypi:`pytest-helper` *last release*: May 31, 2019, *status*: 5 - Production/Stable, @@ -3675,12 +5534,19 @@ This list contains 963 plugins. pytest helpers :pypi:`pytest-helpers-namespace` - *last release*: Apr 29, 2021, + *last release*: Dec 29, 2021, *status*: 5 - Production/Stable, *requires*: pytest (>=6.0.0) Pytest Helpers Namespace Plugin + :pypi:`pytest-henry` + *last release*: Aug 29, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-hidecaptured` *last release*: May 04, 2018, *status*: 4 - Beta, @@ -3688,6 +5554,13 @@ This list contains 963 plugins. Hide captured output + :pypi:`pytest-himark` + *last release*: Apr 14, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin that will filter pytest's test collection using a json file. It will read a json file provided with a --json argument in pytest command line (or in pytest.ini), search the markers key and automatically add -m option to the command line for filtering out the tests marked with disabled markers. + :pypi:`pytest-historic` *last release*: Apr 08, 2020, *status*: N/A, @@ -3702,6 +5575,20 @@ This list contains 963 plugins. Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report + :pypi:`pytest-history` + *last release*: Jan 14, 2024, + *status*: N/A, + *requires*: pytest (>=7.4.3,<8.0.0) + + Pytest plugin to keep a history of your pytest runs + + :pypi:`pytest-home` + *last release*: Oct 09, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest + + Home directory fixtures + :pypi:`pytest-homeassistant` *last release*: Aug 12, 2020, *status*: 4 - Beta, @@ -3710,12 +5597,19 @@ This list contains 963 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Nov 20, 2021, + *last release*: Apr 13, 2024, *status*: 3 - Alpha, - *requires*: pytest (==6.2.5) + *requires*: pytest==8.1.1 Experimental package to automatically extract test plugins for Home Assistant custom components + :pypi:`pytest-honey` + *last release*: Jan 07, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A simple plugin to use with pytest + :pypi:`pytest-honors` *last release*: Mar 06, 2020, *status*: 4 - Beta, @@ -3723,31 +5617,59 @@ This list contains 963 plugins. Report on tests that honor constraints, and guard against regressions + :pypi:`pytest-hot-reloading` + *last release*: Apr 18, 2024, + *status*: N/A, + *requires*: N/A + + + + :pypi:`pytest-hot-test` + *last release*: Dec 10, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A plugin that tracks test changes + + :pypi:`pytest-houdini` + *last release*: Feb 09, 2024, + *status*: N/A, + *requires*: pytest + + pytest plugin for testing code in Houdini. + :pypi:`pytest-hoverfly` - *last release*: Jul 12, 2021, + *last release*: Jan 30, 2023, *status*: N/A, *requires*: pytest (>=5.0) Simplify working with Hoverfly from pytest :pypi:`pytest-hoverfly-wrapper` - *last release*: Aug 29, 2021, - *status*: 4 - Beta, - *requires*: N/A + *last release*: Feb 27, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=3.7.0) Integrates the Hoverfly HTTP proxy into Pytest :pypi:`pytest-hpfeeds` - *last release*: Aug 27, 2021, + *last release*: Feb 28, 2023, *status*: 4 - Beta, *requires*: pytest (>=6.2.4,<7.0.0) Helpers for testing hpfeeds in your python project :pypi:`pytest-html` - *last release*: Dec 13, 2020, + *last release*: Nov 07, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (!=6.0.0,>=5.0) + *requires*: pytest>=7.0.0 + + pytest plugin for generating HTML reports + + :pypi:`pytest-html-cn` + *last release*: Aug 01, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A pytest plugin for generating HTML reports @@ -3758,6 +5680,20 @@ This list contains 963 plugins. optimized pytest plugin for generating HTML reports + :pypi:`pytest-html-merger` + *last release*: Nov 11, 2023, + *status*: N/A, + *requires*: N/A + + Pytest HTML reports merging utility + + :pypi:`pytest-html-object-storage` + *last release*: Jan 17, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + Pytest report plugin for send HTML report on object-storage + :pypi:`pytest-html-profiling` *last release*: Feb 11, 2020, *status*: 5 - Production/Stable, @@ -3766,12 +5702,19 @@ This list contains 963 plugins. Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. :pypi:`pytest-html-reporter` - *last release*: Apr 25, 2021, + *last release*: Feb 13, 2022, *status*: N/A, *requires*: N/A Generates a static html report based on pytest framework + :pypi:`pytest-html-report-merger` + *last release*: Oct 23, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-html-thread` *last release*: Dec 29, 2020, *status*: 5 - Production/Stable, @@ -3787,12 +5730,19 @@ This list contains 963 plugins. Fixture "http" for http requests :pypi:`pytest-httpbin` - *last release*: Feb 11, 2019, + *last release*: May 08, 2023, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest ; extra == 'test' Easily test your HTTP library against a local copy of httpbin + :pypi:`pytest-httpdbg` + *last release*: Jan 10, 2024, + *status*: 3 - Alpha, + *requires*: pytest >=7.0.0 + + A pytest plugin to record HTTP(S) requests with stack trace + :pypi:`pytest-http-mocker` *last release*: Oct 20, 2019, *status*: N/A, @@ -3807,27 +5757,41 @@ This list contains 963 plugins. A thin wrapper of HTTPretty for pytest - :pypi:`pytest-httpserver` - *last release*: Oct 18, 2021, + :pypi:`pytest_httpserver` + *last release*: Feb 24, 2024, *status*: 3 - Alpha, - *requires*: pytest ; extra == 'dev' + *requires*: N/A pytest-httpserver is a httpserver for pytest + :pypi:`pytest-httptesting` + *last release*: Jul 24, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0,<8.0.0) + + http_testing framework on top of pytest + :pypi:`pytest-httpx` - *last release*: Nov 16, 2021, + *last release*: Feb 21, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (==6.*) + *requires*: pytest <9,>=7 Send responses to httpx. :pypi:`pytest-httpx-blockage` - *last release*: Nov 16, 2021, + *last release*: Feb 16, 2023, *status*: N/A, - *requires*: pytest (>=6.2.5) + *requires*: pytest (>=7.2.1) Disable httpx requests during a test run + :pypi:`pytest-httpx-recorder` + *last release*: Jan 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest + + Recorder feature based on pytest_httpx, like recorder feature in responses. + :pypi:`pytest-hue` *last release*: May 09, 2019, *status*: N/A, @@ -3849,17 +5813,24 @@ This list contains 963 plugins. help hypo module for pytest + :pypi:`pytest-iam` + *last release*: Apr 12, 2024, + *status*: 3 - Alpha, + *requires*: pytest>=7.0.0 + + A fully functional OAUTH2 / OpenID Connect (OIDC) server to be used in your testsuite + :pypi:`pytest-ibutsu` - *last release*: Jun 16, 2021, + *last release*: Aug 05, 2022, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest>=7.1 A plugin to sent pytest results to an Ibutsu server :pypi:`pytest-icdiff` - *last release*: Apr 08, 2020, + *last release*: Dec 05, 2023, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest use icdiff for better error messages in pytest assertions @@ -3870,27 +5841,48 @@ This list contains 963 plugins. A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api + :pypi:`pytest-idem` + *last release*: Dec 13, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + A pytest plugin to help with testing idem projects + :pypi:`pytest-idempotent` - *last release*: Nov 26, 2021, + *last release*: Jul 25, 2022, *status*: N/A, *requires*: N/A Pytest plugin for testing function idempotence. :pypi:`pytest-ignore-flaky` - *last release*: Apr 23, 2021, + *last release*: Apr 08, 2024, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest>=6.0 ignore failures from flaky tests (pytest plugin) + :pypi:`pytest-ignore-test-results` + *last release*: Aug 17, 2023, + *status*: 2 - Pre-Alpha, + *requires*: pytest>=7.0 + + A pytest plugin to ignore test results. + :pypi:`pytest-image-diff` - *last release*: Jul 28, 2021, + *last release*: Mar 09, 2023, *status*: 3 - Alpha, *requires*: pytest + :pypi:`pytest-image-snapshot` + *last release*: Dec 01, 2023, + *status*: 4 - Beta, + *requires*: pytest >=3.5.0 + + A pytest plugin for image snapshot management and comparison. + :pypi:`pytest-incremental` *last release*: Apr 24, 2021, *status*: 5 - Production/Stable, @@ -3912,6 +5904,13 @@ This list contains 963 plugins. pytest plugin to collect information from tests + :pypi:`pytest-info-plugin` + *last release*: Sep 14, 2023, + *status*: N/A, + *requires*: N/A + + Get executed interface information in pytest interface automation framework + :pypi:`pytest-informative-node` *last release*: Apr 25, 2019, *status*: 4 - Beta, @@ -3927,26 +5926,54 @@ This list contains 963 plugins. pytest stack validation prior to testing executing :pypi:`pytest-ini` - *last release*: Sep 30, 2021, + *last release*: Apr 26, 2022, *status*: N/A, *requires*: N/A Reuse pytest.ini to store env variables + :pypi:`pytest-initry` + *last release*: Apr 14, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.1.1 + + Plugin for sending automation test data from Pytest to the initry + + :pypi:`pytest-inline` + *last release*: Oct 19, 2023, + *status*: 4 - Beta, + *requires*: pytest >=7.0.0 + + A pytest plugin for writing inline tests. + :pypi:`pytest-inmanta` - *last release*: Aug 17, 2021, + *last release*: Dec 13, 2023, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest A py.test plugin providing fixtures to simplify inmanta modules testing. :pypi:`pytest-inmanta-extensions` - *last release*: May 27, 2021, + *last release*: Apr 02, 2024, *status*: 5 - Production/Stable, *requires*: N/A Inmanta tests package + :pypi:`pytest-inmanta-lsm` + *last release*: Apr 15, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + Common fixtures for inmanta LSM related modules + + :pypi:`pytest-inmanta-yang` + *last release*: Feb 22, 2024, + *status*: 4 - Beta, + *requires*: pytest + + Common fixtures used in inmanta yang related modules + :pypi:`pytest-Inomaly` *last release*: Feb 13, 2018, *status*: 4 - Beta, @@ -3954,17 +5981,31 @@ This list contains 963 plugins. A simple image diff plugin for pytest + :pypi:`pytest-in-robotframework` + *last release*: Mar 02, 2024, + *status*: N/A, + *requires*: pytest + + The extension enables easy execution of pytest tests within the Robot Framework environment. + + :pypi:`pytest-insper` + *last release*: Mar 21, 2024, + *status*: N/A, + *requires*: pytest + + Pytest plugin for courses at Insper + :pypi:`pytest-insta` - *last release*: Apr 07, 2021, + *last release*: Feb 19, 2024, *status*: N/A, - *requires*: pytest (>=6.0.2,<7.0.0) + *requires*: pytest (>=7.2.0,<9.0.0) A practical snapshot testing plugin for pytest :pypi:`pytest-instafail` - *last release*: Jun 14, 2020, + *last release*: Mar 31, 2023, *status*: 4 - Beta, - *requires*: pytest (>=2.9) + *requires*: pytest (>=5) pytest plugin to show failures instantly @@ -3976,16 +6017,16 @@ This list contains 963 plugins. pytest plugin to instrument tests :pypi:`pytest-integration` - *last release*: Apr 16, 2020, + *last release*: Nov 17, 2022, *status*: N/A, *requires*: N/A Organizing pytests by integration or not :pypi:`pytest-integration-mark` - *last release*: Jul 19, 2021, + *last release*: May 22, 2023, *status*: N/A, - *requires*: pytest (>=5.2,<7.0) + *requires*: pytest (>=5.2) Automatic integration test marking and excluding plugin for pytest @@ -4003,10 +6044,17 @@ This list contains 963 plugins. Pytest plugin for intercepting outgoing connection requests during pytest run. + :pypi:`pytest-interface-tester` + *last release*: Feb 09, 2024, + *status*: 4 - Beta, + *requires*: pytest + + Pytest plugin for checking charm relation interface protocol compliance. + :pypi:`pytest-invenio` - *last release*: May 11, 2021, + *last release*: Feb 28, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (<7,>=6) + *requires*: pytest <7.2.0,>=6 Pytest fixtures for Invenio. @@ -4018,7 +6066,7 @@ This list contains 963 plugins. Run tests covering a specific file or changeset :pypi:`pytest-ipdb` - *last release*: Sep 02, 2014, + *last release*: Mar 20, 2013, *status*: 2 - Pre-Alpha, *requires*: N/A @@ -4031,15 +6079,29 @@ This list contains 963 plugins. THIS PROJECT IS ABANDONED + :pypi:`pytest-ipywidgets` + *last release*: Apr 08, 2024, + *status*: N/A, + *requires*: pytest + + + + :pypi:`pytest-isolate` + *last release*: Feb 20, 2023, + *status*: 4 - Beta, + *requires*: pytest + + + :pypi:`pytest-isort` - *last release*: Apr 27, 2021, + *last release*: Mar 05, 2024, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest (>=5.0) py.test plugin to check import ordering using isort :pypi:`pytest-it` - *last release*: Jan 22, 2020, + *last release*: Jan 29, 2024, *status*: 4 - Beta, *requires*: N/A @@ -4052,6 +6114,20 @@ This list contains 963 plugins. Nicer list and iterable assertion messages for pytest + :pypi:`pytest-iters` + *last release*: May 24, 2022, + *status*: N/A, + *requires*: N/A + + A contextmanager pytest fixture for handling multiple mock iters + + :pypi:`pytest_jar_yuan` + *last release*: Dec 12, 2022, + *status*: N/A, + *requires*: N/A + + A allure and pytest used package + :pypi:`pytest-jasmine` *last release*: Nov 04, 2017, *status*: 1 - Planning, @@ -4059,6 +6135,13 @@ This list contains 963 plugins. Run jasmine tests from your pytest test suite + :pypi:`pytest-jelastic` + *last release*: Nov 16, 2022, + *status*: N/A, + *requires*: pytest (>=7.2.0,<8.0.0) + + Pytest plugin defining the necessary command-line options to pass to pytests testing a Jelastic environment. + :pypi:`pytest-jest` *last release*: May 22, 2018, *status*: 4 - Beta, @@ -4066,20 +6149,41 @@ This list contains 963 plugins. A custom jest-pytest oriented Pytest reporter + :pypi:`pytest-jinja` + *last release*: Oct 04, 2022, + *status*: 3 - Alpha, + *requires*: pytest (>=6.2.5,<7.0.0) + + A plugin to generate customizable jinja-based HTML reports in pytest + :pypi:`pytest-jira` - *last release*: Dec 02, 2021, + *last release*: Apr 12, 2024, *status*: 3 - Alpha, *requires*: N/A py.test JIRA integration plugin, using markers + :pypi:`pytest-jira-xfail` + *last release*: Jun 19, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0) + + Plugin skips (xfail) tests if unresolved Jira issue(s) linked + :pypi:`pytest-jira-xray` - *last release*: Nov 28, 2021, - *status*: 3 - Alpha, - *requires*: pytest + *last release*: Mar 27, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.4 pytest plugin to integrate tests with JIRA XRAY + :pypi:`pytest-job-selection` + *last release*: Jan 30, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A pytest plugin for load balancing test suites + :pypi:`pytest-jobserver` *last release*: May 15, 2019, *status*: 5 - Production/Stable, @@ -4101,6 +6205,13 @@ This list contains 963 plugins. Generate JSON test reports + :pypi:`pytest-json-fixtures` + *last release*: Mar 14, 2023, + *status*: 4 - Beta, + *requires*: N/A + + JSON output for the --fixtures flag + :pypi:`pytest-jsonlint` *last release*: Aug 04, 2016, *status*: N/A, @@ -4109,14 +6220,49 @@ This list contains 963 plugins. UNKNOWN :pypi:`pytest-json-report` - *last release*: Sep 24, 2021, + *last release*: Mar 15, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.8.0) A pytest plugin to report test results as JSON files + :pypi:`pytest-json-report-wip` + *last release*: Oct 28, 2023, + *status*: 4 - Beta, + *requires*: pytest >=3.8.0 + + A pytest plugin to report test results as JSON files + + :pypi:`pytest-jsonschema` + *last release*: Mar 27, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A pytest plugin to perform JSONSchema validations + + :pypi:`pytest-jtr` + *last release*: Apr 15, 2024, + *status*: N/A, + *requires*: pytest<8.0.0,>=7.1.2 + + pytest plugin supporting json test report output + + :pypi:`pytest-jupyter` + *last release*: Apr 04, 2024, + *status*: 4 - Beta, + *requires*: pytest>=7.0 + + A pytest plugin for testing Jupyter libraries and extensions. + + :pypi:`pytest-jupyterhub` + *last release*: Apr 25, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest + + A reusable JupyterHub pytest plugin + :pypi:`pytest-kafka` - *last release*: Aug 24, 2021, + *last release*: Jun 14, 2023, *status*: N/A, *requires*: pytest @@ -4129,8 +6275,36 @@ This list contains 963 plugins. A plugin to send pytest events to Kafka + :pypi:`pytest-kasima` + *last release*: Jan 26, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.2.1,<8.0.0) + + Display horizontal lines above and below the captured standard output for easy viewing. + + :pypi:`pytest-keep-together` + *last release*: Dec 07, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest plugin to customize test ordering by running all 'related' tests together + + :pypi:`pytest-kexi` + *last release*: Apr 29, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.2,<8.0.0) + + + + :pypi:`pytest-keyring` + *last release*: Oct 01, 2023, + *status*: N/A, + *requires*: pytest (>=7.1) + + A Pytest plugin to access the system's keyring to provide credentials for tests + :pypi:`pytest-kind` - *last release*: Jan 24, 2021, + *last release*: Nov 30, 2022, *status*: 5 - Production/Stable, *requires*: N/A @@ -4157,6 +6331,13 @@ This list contains 963 plugins. Run Konira DSL tests with py.test + :pypi:`pytest-koopmans` + *last release*: Nov 21, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A plugin for testing the koopmans package + :pypi:`pytest-krtech-common` *last release*: Nov 28, 2016, *status*: 4 - Beta, @@ -4164,6 +6345,20 @@ This list contains 963 plugins. pytest krtech common library + :pypi:`pytest-kubernetes` + *last release*: Sep 14, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.1,<8.0.0) + + + + :pypi:`pytest-kuunda` + *last release*: Feb 25, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + pytest plugin to help with test data setup for PySpark tests + :pypi:`pytest-kwparametrize` *last release*: Jan 22, 2021, *status*: N/A, @@ -4172,9 +6367,9 @@ This list contains 963 plugins. Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks :pypi:`pytest-lambda` - *last release*: Aug 23, 2021, + *last release*: Aug 20, 2022, *status*: 3 - Alpha, - *requires*: pytest (>=3.6,<7) + *requires*: pytest (>=3.6,<8) Define pytest fixtures with lambda functions. @@ -4185,6 +6380,27 @@ This list contains 963 plugins. + :pypi:`pytest-langchain` + *last release*: Feb 26, 2023, + *status*: N/A, + *requires*: pytest + + Pytest-style test runner for langchain agents + + :pypi:`pytest-lark` + *last release*: Nov 05, 2023, + *status*: N/A, + *requires*: N/A + + Create fancy and clear HTML test reports. + + :pypi:`pytest-launchable` + *last release*: Apr 05, 2023, + *status*: N/A, + *requires*: pytest (>=4.2.0) + + Launchable Pytest Plugin + :pypi:`pytest-layab` *last release*: Oct 05, 2020, *status*: 5 - Production/Stable, @@ -4199,6 +6415,13 @@ This list contains 963 plugins. It helps to use fixtures in pytest.mark.parametrize + :pypi:`pytest-lazy-fixtures` + *last release*: Mar 16, 2024, + *status*: N/A, + *requires*: pytest (>=7) + + Allows you to use fixtures in @pytest.mark.parametrize. + :pypi:`pytest-ldap` *last release*: Aug 18, 2020, *status*: N/A, @@ -4206,6 +6429,13 @@ This list contains 963 plugins. python-ldap fixtures for pytest + :pypi:`pytest-leak-finder` + *last release*: Feb 15, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + Find the test that's leaking before the one that fails + :pypi:`pytest-leaks` *last release*: Nov 27, 2019, *status*: 1 - Planning, @@ -4213,6 +6443,13 @@ This list contains 963 plugins. A pytest plugin to trace resource leaks. + :pypi:`pytest-leaping` + *last release*: Mar 27, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A simple plugin to use with pytest + :pypi:`pytest-level` *last release*: Oct 21, 2019, *status*: N/A, @@ -4221,14 +6458,14 @@ This list contains 963 plugins. Select tests of a given level or lower :pypi:`pytest-libfaketime` - *last release*: Dec 22, 2018, + *last release*: Apr 12, 2024, *status*: 4 - Beta, - *requires*: pytest (>=3.0.0) + *requires*: pytest>=3.0.0 - A python-libfaketime plugin for pytest. + A python-libfaketime plugin for pytest :pypi:`pytest-libiio` - *last release*: Oct 29, 2021, + *last release*: Dec 22, 2023, *status*: 4 - Beta, *requires*: N/A @@ -4256,8 +6493,15 @@ This list contains 963 plugins. A pytest plugin to show the line numbers of test functions :pypi:`pytest-line-profiler` - *last release*: May 03, 2021, + *last release*: Aug 10, 2023, *status*: 4 - Beta, + *requires*: pytest >=3.5.0 + + Profile code executed by pytest + + :pypi:`pytest-line-profiler-apn` + *last release*: Dec 05, 2022, + *status*: N/A, *requires*: pytest (>=3.5.0) Profile code executed by pytest @@ -4283,6 +6527,13 @@ This list contains 963 plugins. A pytest plugin that stream output in LITF format + :pypi:`pytest-litter` + *last release*: Nov 23, 2023, + *status*: 4 - Beta, + *requires*: pytest >=6.1 + + Pytest plugin which verifies that tests do not modify file trees. + :pypi:`pytest-live` *last release*: Mar 08, 2020, *status*: N/A, @@ -4290,29 +6541,43 @@ This list contains 963 plugins. Live results for pytest + :pypi:`pytest-local-badge` + *last release*: Jan 15, 2023, + *status*: N/A, + *requires*: pytest (>=6.1.0) + + Generate local badges (shields) reporting your test suite status. + :pypi:`pytest-localftpserver` - *last release*: Aug 25, 2021, + *last release*: Oct 14, 2023, *status*: 5 - Production/Stable, *requires*: pytest A PyTest plugin which provides an FTP fixture for your tests :pypi:`pytest-localserver` - *last release*: Nov 19, 2021, + *last release*: Oct 12, 2023, *status*: 4 - Beta, *requires*: N/A - py.test plugin to test server connections locally. + pytest plugin to test server connections locally. :pypi:`pytest-localstack` - *last release*: Aug 22, 2019, + *last release*: Jun 07, 2023, *status*: 4 - Beta, - *requires*: pytest (>=3.3.0) + *requires*: pytest (>=6.0.0,<7.0.0) Pytest plugin for AWS integration tests + :pypi:`pytest-lock` + *last release*: Feb 03, 2024, + *status*: N/A, + *requires*: pytest (>=7.4.3,<8.0.0) + + pytest-lock is a pytest plugin that allows you to "lock" the results of unit tests, storing them in a local cache. This is particularly useful for tests that are resource-intensive or don't need to be run every time. When the tests are run subsequently, pytest-lock will compare the current results with the locked results and issue a warning if there are any discrepancies. + :pypi:`pytest-lockable` - *last release*: Nov 09, 2021, + *last release*: Jan 24, 2024, *status*: 5 - Production/Stable, *requires*: pytest @@ -4354,8 +6619,8 @@ This list contains 963 plugins. Pytest plugin providing three logger fixtures with basic or full writing to log files :pypi:`pytest-logger` - *last release*: Jul 25, 2019, - *status*: 4 - Beta, + *last release*: Mar 10, 2024, + *status*: 5 - Production/Stable, *requires*: pytest (>=3.2) Plugin configuring handlers for loggers from Python logging module. @@ -4367,6 +6632,20 @@ This list contains 963 plugins. Configures logging and allows tweaking the log level with a py.test flag + :pypi:`pytest-logging-end-to-end-test-tool` + *last release*: Sep 23, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.2,<8.0.0) + + + + :pypi:`pytest-logikal` + *last release*: Mar 30, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest==8.1.1 + + Common testing environment + :pypi:`pytest-log-report` *last release*: Dec 26, 2019, *status*: N/A, @@ -4374,13 +6653,41 @@ This list contains 963 plugins. Package for creating a pytest test run reprot + :pypi:`pytest-loguru` + *last release*: Mar 20, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest; extra == "test" + + Pytest Loguru + + :pypi:`pytest-loop` + *last release*: Mar 30, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest + + pytest plugin for looping tests + + :pypi:`pytest-lsp` + *last release*: Feb 07, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + A pytest plugin for end-to-end testing of language servers + :pypi:`pytest-manual-marker` - *last release*: Oct 11, 2021, + *last release*: Aug 04, 2022, *status*: 3 - Alpha, - *requires*: pytest (>=6) + *requires*: pytest>=7 pytest marker for marking manual tests + :pypi:`pytest-markdoctest` + *last release*: Jul 22, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=6) + + A pytest plugin to doctest your markdown files + :pypi:`pytest-markdown` *last release*: Jan 15, 2021, *status*: 4 - Beta, @@ -4388,6 +6695,13 @@ This list contains 963 plugins. Test your markdown docs with pytest + :pypi:`pytest-markdown-docs` + *last release*: Mar 05, 2024, + *status*: N/A, + *requires*: pytest (>=7.0.0) + + Run markdown code fences through pytest + :pypi:`pytest-marker-bugzilla` *last release*: Jan 09, 2020, *status*: N/A, @@ -4424,11 +6738,11 @@ This list contains 963 plugins. UNKNOWN :pypi:`pytest-matcher` - *last release*: Apr 23, 2020, + *last release*: Mar 15, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.4) + *requires*: pytest - Match test output against patterns stored in files + Easy way to match captured \`pytest\` output against expectations stored in files :pypi:`pytest-match-skip` *last release*: May 15, 2019, @@ -4451,6 +6765,27 @@ This list contains 963 plugins. Provide tools for generating tests from combinations of fixtures. + :pypi:`pytest-maxcov` + *last release*: Sep 24, 2023, + *status*: N/A, + *requires*: pytest (>=7.4.0,<8.0.0) + + Compute the maximum coverage available through pytest with the minimum execution time cost + + :pypi:`pytest-maybe-context` + *last release*: Apr 16, 2023, + *status*: N/A, + *requires*: pytest (>=7,<8) + + Simplify tests with warning and exception cases. + + :pypi:`pytest-maybe-raises` + *last release*: May 27, 2022, + *status*: N/A, + *requires*: pytest ; extra == 'dev' + + Pytest fixture for optional exception testing. + :pypi:`pytest-mccabe` *last release*: Jul 22, 2020, *status*: 3 - Alpha, @@ -4466,12 +6801,26 @@ This list contains 963 plugins. Plugin for generating Markdown reports for pytest results :pypi:`pytest-md-report` - *last release*: May 04, 2021, + *last release*: Feb 04, 2024, *status*: 4 - Beta, - *requires*: pytest (!=6.0.0,<7,>=3.3.2) + *requires*: pytest !=6.0.0,<9,>=3.3.2 A pytest plugin to make a test results report with Markdown table format. + :pypi:`pytest-meilisearch` + *last release*: Feb 15, 2024, + *status*: N/A, + *requires*: pytest (>=7.4.3) + + Pytest helpers for testing projects using Meilisearch + + :pypi:`pytest-memlog` + *last release*: May 03, 2023, + *status*: N/A, + *requires*: pytest (>=7.3.0,<8.0.0) + + Log memory usage during tests + :pypi:`pytest-memprof` *last release*: Mar 29, 2019, *status*: 4 - Beta, @@ -4479,6 +6828,13 @@ This list contains 963 plugins. Estimates memory consumption of test functions + :pypi:`pytest-memray` + *last release*: Apr 18, 2024, + *status*: N/A, + *requires*: pytest>=7.2 + + A simple plugin to use with pytest + :pypi:`pytest-menu` *last release*: Oct 04, 2017, *status*: 3 - Alpha, @@ -4493,24 +6849,31 @@ This list contains 963 plugins. pytest plugin to write integration tests for projects using Mercurial Python internals + :pypi:`pytest-mesh` + *last release*: Aug 05, 2022, + *status*: N/A, + *requires*: pytest (==7.1.2) + + pytest_mesh插件 + :pypi:`pytest-message` - *last release*: Nov 04, 2021, + *last release*: Aug 04, 2022, *status*: N/A, *requires*: pytest (>=6.2.5) Pytest plugin for sending report message of marked tests execution :pypi:`pytest-messenger` - *last release*: Dec 16, 2020, + *last release*: Nov 24, 2022, *status*: 5 - Production/Stable, *requires*: N/A Pytest to Slack reporting plugin :pypi:`pytest-metadata` - *last release*: Nov 27, 2020, + *last release*: Feb 12, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.9.0) + *requires*: pytest>=7.0.0 pytest plugin for test session metadata @@ -4521,6 +6884,13 @@ This list contains 963 plugins. Custom metrics report for pytest + :pypi:`pytest-mh` + *last release*: Mar 14, 2024, + *status*: N/A, + *requires*: pytest + + Pytest multihost plugin + :pypi:`pytest-mimesis` *last release*: Mar 21, 2020, *status*: 5 - Production/Stable, @@ -4529,12 +6899,26 @@ This list contains 963 plugins. Mimesis integration with the pytest test runner :pypi:`pytest-minecraft` - *last release*: Sep 26, 2020, + *last release*: Apr 06, 2022, *status*: N/A, - *requires*: pytest (>=6.0.1,<7.0.0) + *requires*: pytest (>=6.0.1) A pytest plugin for running tests against Minecraft releases + :pypi:`pytest-mini` + *last release*: Feb 06, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0,<8.0.0) + + A plugin to test mp + + :pypi:`pytest-minio-mock` + *last release*: Apr 15, 2024, + *status*: N/A, + *requires*: pytest>=5.0.0 + + A pytest plugin for mocking Minio S3 interactions + :pypi:`pytest-missing-fixtures` *last release*: Oct 14, 2020, *status*: 4 - Beta, @@ -4542,6 +6926,13 @@ This list contains 963 plugins. Pytest plugin that creates missing fixtures + :pypi:`pytest-mitmproxy` + *last release*: Mar 07, 2024, + *status*: N/A, + *requires*: pytest >=7.0 + + pytest plugin for mitmproxy tests + :pypi:`pytest-ml` *last release*: May 04, 2019, *status*: 4 - Beta, @@ -4557,9 +6948,9 @@ This list contains 963 plugins. pytest plugin to display test execution output like a mochajs :pypi:`pytest-mock` - *last release*: May 06, 2021, + *last release*: Mar 21, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.0) + *requires*: pytest>=6.2.5 Thin-wrapper around the mock package for easier use with pytest @@ -4571,7 +6962,7 @@ This list contains 963 plugins. A mock API server with configurable routes and responses available as a fixture. :pypi:`pytest-mock-generator` - *last release*: Aug 10, 2021, + *last release*: May 16, 2022, *status*: 5 - Production/Stable, *requires*: N/A @@ -4599,16 +6990,16 @@ This list contains 963 plugins. An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. :pypi:`pytest-mock-resources` - *last release*: Dec 03, 2021, + *last release*: Apr 11, 2024, *status*: N/A, - *requires*: pytest (>=1.0) + *requires*: pytest>=1.0 A pytest plugin for easily instantiating reproducible mock resources. :pypi:`pytest-mock-server` - *last release*: Apr 06, 2020, + *last release*: Jan 09, 2022, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest (>=3.5.0) Mock server plugin for pytest @@ -4619,6 +7010,27 @@ This list contains 963 plugins. A set of fixtures to test your requests to HTTP/UDP servers + :pypi:`pytest-mocktcp` + *last release*: Oct 11, 2022, + *status*: N/A, + *requires*: pytest + + A pytest plugin for testing TCP clients + + :pypi:`pytest-modalt` + *last release*: Feb 27, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + Massively distributed pytest runs using modal.com + + :pypi:`pytest-modified-env` + *last release*: Jan 29, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Pytest plugin to fail a test if it leaves modified \`os.environ\` afterwards. + :pypi:`pytest-modifyjunit` *last release*: Jan 10, 2019, *status*: N/A, @@ -4634,28 +7046,35 @@ This list contains 963 plugins. pytest plugin to modify fixture scope :pypi:`pytest-molecule` - *last release*: Oct 06, 2021, + *last release*: Mar 29, 2022, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest (>=7.0.0) + + PyTest Molecule Plugin :: discover and run molecule tests + + :pypi:`pytest-molecule-JC` + *last release*: Jul 18, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.0.0) PyTest Molecule Plugin :: discover and run molecule tests :pypi:`pytest-mongo` - *last release*: Jun 07, 2021, + *last release*: Mar 13, 2024, *status*: 5 - Production/Stable, - *requires*: pytest + *requires*: pytest >=6.2 MongoDB process and client fixtures plugin for Pytest. :pypi:`pytest-mongodb` - *last release*: Dec 07, 2019, + *last release*: May 16, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.5.2) + *requires*: N/A pytest plugin for MongoDB fixtures :pypi:`pytest-monitor` - *last release*: Aug 24, 2021, + *last release*: Jun 25, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -4697,32 +7116,39 @@ This list contains 963 plugins. A test batcher for multiprocessed Pytest runs :pypi:`pytest-mpi` - *last release*: Mar 14, 2021, + *last release*: Jan 08, 2022, *status*: 3 - Alpha, *requires*: pytest pytest plugin to collect information from tests + :pypi:`pytest-mpiexec` + *last release*: Apr 13, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + pytest plugin for running individual tests with mpiexec + :pypi:`pytest-mpl` - *last release*: Jul 02, 2021, + *last release*: Feb 14, 2024, *status*: 4 - Beta, *requires*: pytest pytest plugin to help with testing figures output from Matplotlib :pypi:`pytest-mproc` - *last release*: Mar 07, 2021, + *last release*: Nov 15, 2022, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest (>=6) low-startup-overhead, scalable, distributed-testing pytest plugin - :pypi:`pytest-multi-check` - *last release*: Jun 03, 2021, - *status*: N/A, - *requires*: pytest + :pypi:`pytest-mqtt` + *last release*: Mar 31, 2024, + *status*: 4 - Beta, + *requires*: pytest<8; extra == "test" - Pytest-плагин, реализует возможность мульти проверок и мягких проверок + pytest-mqtt supports testing systems based on MQTT :pypi:`pytest-multihost` *last release*: Apr 07, 2020, @@ -4732,19 +7158,26 @@ This list contains 963 plugins. Utility for writing multi-host tests for pytest :pypi:`pytest-multilog` - *last release*: Jun 10, 2021, + *last release*: Jan 17, 2023, *status*: N/A, - *requires*: N/A + *requires*: pytest Multi-process logs handling and other helpers for pytest :pypi:`pytest-multithreading` - *last release*: Aug 12, 2021, + *last release*: Dec 07, 2022, *status*: N/A, - *requires*: pytest (>=3.6) + *requires*: N/A a pytest plugin for th and concurrent testing + :pypi:`pytest-multithreading-allure` + *last release*: Nov 25, 2022, + *status*: N/A, + *requires*: N/A + + pytest_multithreading_allure + :pypi:`pytest-mutagen` *last release*: Jul 24, 2020, *status*: N/A, @@ -4752,10 +7185,17 @@ This list contains 963 plugins. Add the mutation testing feature to pytest + :pypi:`pytest-my-cool-lib` + *last release*: Nov 02, 2023, + *status*: N/A, + *requires*: pytest (>=7.1.3,<8.0.0) + + + :pypi:`pytest-mypy` - *last release*: Mar 21, 2021, + *last release*: Dec 18, 2022, *status*: 4 - Beta, - *requires*: pytest (>=3.5) + *requires*: pytest (>=6.2) ; python_version >= "3.10" Mypy static type checker plugin for Pytest @@ -4767,33 +7207,40 @@ This list contains 963 plugins. Mypy static type checker plugin for Pytest :pypi:`pytest-mypy-plugins` - *last release*: Oct 19, 2021, - *status*: 3 - Alpha, - *requires*: pytest (>=6.0.0) + *last release*: Mar 31, 2024, + *status*: 4 - Beta, + *requires*: pytest>=7.0.0 pytest plugin for writing tests for mypy plugins :pypi:`pytest-mypy-plugins-shim` *last release*: Apr 12, 2021, *status*: N/A, - *requires*: N/A + *requires*: pytest>=6.0.0 Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. :pypi:`pytest-mypy-testing` - *last release*: Jun 13, 2021, + *last release*: Mar 04, 2024, *status*: N/A, - *requires*: pytest + *requires*: pytest>=7,<9 Pytest plugin to check mypy output. :pypi:`pytest-mysql` - *last release*: Nov 22, 2021, + *last release*: Oct 30, 2023, *status*: 5 - Production/Stable, - *requires*: pytest + *requires*: pytest >=6.2 MySQL process and client fixtures for pytest + :pypi:`pytest-ndb` + *last release*: Oct 15, 2023, + *status*: N/A, + *requires*: pytest + + pytest notebook debugger + :pypi:`pytest-needle` *last release*: Dec 10, 2018, *status*: 4 - Beta, @@ -4802,12 +7249,26 @@ This list contains 963 plugins. pytest plugin for visual testing websites using selenium :pypi:`pytest-neo` - *last release*: Apr 23, 2019, + *last release*: Jan 08, 2022, *status*: 3 - Alpha, - *requires*: pytest (>=3.7.2) + *requires*: pytest (>=6.2.0) pytest-neo is a plugin for pytest that shows tests like screen of Matrix. + :pypi:`pytest-neos` + *last release*: Apr 15, 2024, + *status*: 1 - Planning, + *requires*: N/A + + Pytest plugin for neos + + :pypi:`pytest-netdut` + *last release*: Mar 07, 2024, + *status*: N/A, + *requires*: pytest <7.3,>=3.5.0 + + "Automated software testing for switches using pytest" + :pypi:`pytest-network` *last release*: May 07, 2020, *status*: N/A, @@ -4815,6 +7276,13 @@ This list contains 963 plugins. A simple plugin to disable network on socket level. + :pypi:`pytest-network-endpoints` + *last release*: Mar 06, 2022, + *status*: N/A, + *requires*: pytest + + Network endpoints plugin for pytest + :pypi:`pytest-never-sleep` *last release*: May 05, 2021, *status*: 3 - Alpha, @@ -4837,9 +7305,9 @@ This list contains 963 plugins. nginx fixture for pytest - iplweb temporary fork :pypi:`pytest-ngrok` - *last release*: Jan 22, 2020, + *last release*: Jan 20, 2022, *status*: 3 - Alpha, - *requires*: N/A + *requires*: pytest @@ -4850,6 +7318,13 @@ This list contains 963 plugins. pytest ngs fixtures + :pypi:`pytest-nhsd-apim` + *last release*: Feb 16, 2024, + *status*: N/A, + *requires*: pytest (>=6.2.5,<7.0.0) + + Pytest plugin accessing NHSDigital's APIM proxies + :pypi:`pytest-nice` *last release*: May 04, 2019, *status*: 4 - Beta, @@ -4864,20 +7339,27 @@ This list contains 963 plugins. A small snippet for nicer PyTest's Parametrize - :pypi:`pytest-nlcov` - *last release*: Jul 07, 2021, + :pypi:`pytest_nlcov` + *last release*: Apr 11, 2024, *status*: N/A, *requires*: N/A Pytest plugin to get the coverage of the new lines (based on git diff) only :pypi:`pytest-nocustom` - *last release*: Jul 07, 2021, + *last release*: Apr 11, 2024, *status*: 5 - Production/Stable, *requires*: N/A Run all tests without custom markers + :pypi:`pytest-node-dependency` + *last release*: Apr 10, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A + + pytest plugin for controlling execution flow + :pypi:`pytest-nodev` *last release*: Jul 21, 2016, *status*: 4 - Beta, @@ -4892,12 +7374,19 @@ This list contains 963 plugins. Ensure a test produces no garbage - :pypi:`pytest-notebook` - *last release*: Sep 16, 2020, + :pypi:`pytest-nose-attrib` + *last release*: Aug 13, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin to use nose @attrib marks decorators and pick tests based on attributes and partially uses nose-attrib plugin approach + + :pypi:`pytest_notebook` + *last release*: Nov 28, 2023, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest>=3.5.0 - A pytest plugin for testing Jupyter Notebooks + A pytest plugin for testing Jupyter Notebooks. :pypi:`pytest-notice` *last release*: Nov 05, 2020, @@ -4920,6 +7409,13 @@ This list contains 963 plugins. A pytest plugin to notify test result + :pypi:`pytest_notify` + *last release*: Jul 05, 2017, + *status*: N/A, + *requires*: pytest>=3.0.0 + + Get notifications when your tests ends + :pypi:`pytest-notimplemented` *last release*: Aug 27, 2019, *status*: N/A, @@ -4935,12 +7431,26 @@ This list contains 963 plugins. A PyTest Reporter to send test runs to Notion.so :pypi:`pytest-nunit` - *last release*: Aug 04, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *last release*: Feb 26, 2024, + *status*: 5 - Production/Stable, + *requires*: N/A A pytest plugin for generating NUnit3 test result XML output + :pypi:`pytest-oar` + *last release*: May 02, 2023, + *status*: N/A, + *requires*: pytest>=6.0.1 + + PyTest plugin for the OAR testing framework + + :pypi:`pytest-object-getter` + *last release*: Jul 31, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Import any object from a 3rd party module while mocking its namespace on demand. + :pypi:`pytest-ochrus` *last release*: Feb 21, 2018, *status*: 4 - Beta, @@ -4948,10 +7458,17 @@ This list contains 963 plugins. pytest results data-base and HTML reporter + :pypi:`pytest-odc` + *last release*: Aug 04, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A pytest plugin for simplifying ODC database tests + :pypi:`pytest-odoo` - *last release*: Nov 04, 2021, + *last release*: Jul 06, 2023, *status*: 4 - Beta, - *requires*: pytest (>=2.9) + *requires*: pytest (>=7.2.0) py.test plugin to run Odoo tests @@ -4969,6 +7486,20 @@ This list contains 963 plugins. pytest plugin to test OpenERP modules + :pypi:`pytest-offline` + *last release*: Mar 09, 2023, + *status*: 1 - Planning, + *requires*: pytest (>=7.0.0,<8.0.0) + + + + :pypi:`pytest-ogsm-plugin` + *last release*: May 16, 2023, + *status*: N/A, + *requires*: N/A + + 针对特定项目定制化插件,优化了pytest报告展示方式,并添加了项目所需特定参数 + :pypi:`pytest-ok` *last release*: Apr 01, 2019, *status*: 4 - Beta, @@ -4977,12 +7508,19 @@ This list contains 963 plugins. The ultimate pytest output plugin :pypi:`pytest-only` - *last release*: Jan 19, 2020, - *status*: N/A, - *requires*: N/A + *last release*: Mar 09, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest (<7.1) ; python_full_version <= "3.6.0" Use @pytest.mark.only to run a single test + :pypi:`pytest-oof` + *last release*: Dec 11, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A Pytest plugin providing structured, programmatic access to a test run's results + :pypi:`pytest-oot` *last release*: Sep 18, 2016, *status*: 4 - Beta, @@ -4997,17 +7535,24 @@ This list contains 963 plugins. Pytest plugin for detecting inadvertent open file handles + :pypi:`pytest-opentelemetry` + *last release*: Oct 01, 2023, + *status*: N/A, + *requires*: pytest + + A pytest plugin for instrumenting test runs via OpenTelemetry + :pypi:`pytest-opentmi` - *last release*: Nov 04, 2021, + *last release*: Jun 02, 2022, *status*: 5 - Production/Stable, *requires*: pytest (>=5.0) pytest plugin for publish results to opentmi :pypi:`pytest-operator` - *last release*: Oct 26, 2021, + *last release*: Sep 28, 2022, *status*: N/A, - *requires*: N/A + *requires*: pytest Fixtures for Operators @@ -5033,9 +7578,9 @@ This list contains 963 plugins. A pytest plugin for orchestrating tests :pypi:`pytest-order` - *last release*: May 30, 2021, + *last release*: Apr 02, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5.0) + *requires*: pytest>=5.0; python_version < "3.10" pytest plugin to run your tests in a specific order @@ -5046,6 +7591,13 @@ This list contains 963 plugins. pytest plugin to run your tests in a specific order + :pypi:`pytest-order-modify` + *last release*: Nov 04, 2022, + *status*: N/A, + *requires*: N/A + + 新增run_marker 来自定义用例的执行顺序 + :pypi:`pytest-osxnotify` *last release*: May 15, 2015, *status*: N/A, @@ -5053,12 +7605,33 @@ This list contains 963 plugins. OS X notifications for py.test results. + :pypi:`pytest-ot` + *last release*: Mar 21, 2024, + *status*: N/A, + *requires*: pytest; extra == "dev" + + A pytest plugin for instrumenting test runs via OpenTelemetry + :pypi:`pytest-otel` - *last release*: Dec 03, 2021, + *last release*: Mar 18, 2024, + *status*: N/A, + *requires*: pytest==8.1.1 + + OpenTelemetry plugin for Pytest + + :pypi:`pytest-override-env-var` + *last release*: Feb 25, 2023, + *status*: N/A, + *requires*: N/A + + Pytest mark to override a value of an environment variable. + + :pypi:`pytest-owner` + *last release*: Apr 25, 2022, *status*: N/A, *requires*: N/A - pytest-otel report OpenTelemetry traces about test executed + Add owner mark for tests :pypi:`pytest-pact` *last release*: Jan 07, 2019, @@ -5088,6 +7661,13 @@ This list contains 963 plugins. a pytest plugin for parallel and concurrent testing + :pypi:`pytest-parallelize-tests` + *last release*: Jan 27, 2023, + *status*: 4 - Beta, + *requires*: N/A + + pytest plugin that parallelizes test execution across multiple hosts + :pypi:`pytest-param` *last release*: Sep 11, 2016, *status*: 4 - Beta, @@ -5102,26 +7682,54 @@ This list contains 963 plugins. Configure pytest fixtures using a combination of"parametrize" and markers + :pypi:`pytest-parameterize-from-files` + *last release*: Feb 15, 2024, + *status*: 4 - Beta, + *requires*: pytest>=7.2.0 + + A pytest plugin that parameterizes tests from data files. + :pypi:`pytest-parametrization` - *last release*: Nov 30, 2021, + *last release*: May 22, 2022, *status*: 5 - Production/Stable, - *requires*: pytest + *requires*: N/A Simpler PyTest parametrization :pypi:`pytest-parametrize-cases` - *last release*: Dec 12, 2020, + *last release*: Mar 13, 2022, *status*: N/A, - *requires*: pytest (>=6.1.2,<7.0.0) + *requires*: pytest (>=6.1.2) A more user-friendly way to write parametrized tests. :pypi:`pytest-parametrized` - *last release*: Oct 19, 2020, + *last release*: Nov 03, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest decorator for parametrizing tests with default iterables. + + :pypi:`pytest-parametrize-suite` + *last release*: Jan 19, 2023, *status*: 5 - Production/Stable, *requires*: pytest - Pytest plugin for parametrizing tests with default iterables. + A simple pytest extension for creating a named test suite. + + :pypi:`pytest_param_files` + *last release*: Jul 29, 2023, + *status*: N/A, + *requires*: pytest + + Create pytest parametrize decorators from external files. + + :pypi:`pytest-param-scope` + *last release*: Oct 18, 2023, + *status*: N/A, + *requires*: pytest + + pytest parametrize scope fixture workaround :pypi:`pytest-parawtf` *last release*: Dec 03, 2018, @@ -5151,6 +7759,13 @@ This list contains 963 plugins. Allow setting the path to a paste config file + :pypi:`pytest-patch` + *last release*: Apr 29, 2023, + *status*: 3 - Alpha, + *requires*: pytest (>=7.0.0) + + An automagic \`patch\` fixture that can patch objects directly or by name. + :pypi:`pytest-patches` *last release*: Aug 30, 2021, *status*: 4 - Beta, @@ -5158,6 +7773,13 @@ This list contains 963 plugins. A contextmanager pytest fixture for handling multiple mock patches + :pypi:`pytest-patterns` + *last release*: Nov 17, 2023, + *status*: 4 - Beta, + *requires*: N/A + + pytest plugin to make testing complicated long string output easy to write and easy to debug + :pypi:`pytest-pdb` *last release*: Jul 31, 2018, *status*: N/A, @@ -5193,12 +7815,19 @@ This list contains 963 plugins. Change the exit code of pytest test sessions when a required percent of tests pass. + :pypi:`pytest-percents` + *last release*: Mar 16, 2024, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-perf` - *last release*: Jun 27, 2021, + *last release*: Jan 28, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.6) ; extra == 'testing' + *requires*: pytest >=6 ; extra == 'testing' - pytest-perf + Run performance tests against the mainline code. :pypi:`pytest-performance` *last release*: Sep 11, 2020, @@ -5207,13 +7836,34 @@ This list contains 963 plugins. A simple plugin to ensure the execution of critical sections of code has not been impacted + :pypi:`pytest-performancetotal` + *last release*: Mar 19, 2024, + *status*: 4 - Beta, + *requires*: N/A + + A performance plugin for pytest + :pypi:`pytest-persistence` - *last release*: Nov 06, 2021, + *last release*: Jul 04, 2023, *status*: N/A, *requires*: N/A Pytest tool for persistent objects + :pypi:`pytest-pexpect` + *last release*: Mar 27, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + Pytest pexpect plugin. + + :pypi:`pytest-pg` + *last release*: Apr 03, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=6.0.0 + + A tiny plugin for pytest which runs PostgreSQL in Docker + :pypi:`pytest-pgsql` *last release*: May 13, 2020, *status*: 5 - Production/Stable, @@ -5222,16 +7872,16 @@ This list contains 963 plugins. Pytest plugins and helpers for tests using a Postgres database. :pypi:`pytest-phmdoctest` - *last release*: Nov 10, 2021, + *last release*: Apr 15, 2022, *status*: 4 - Beta, - *requires*: pytest (>=6.2) ; extra == 'test' + *requires*: pytest (>=5.4.3) pytest plugin to test Python examples in Markdown using phmdoctest. :pypi:`pytest-picked` - *last release*: Dec 23, 2020, + *last release*: Jul 27, 2023, *status*: N/A, - *requires*: pytest (>=3.5.0) + *requires*: pytest (>=3.7.0) Run the tests related to the changed files @@ -5256,6 +7906,13 @@ This list contains 963 plugins. Slice in your test base thanks to powerful markers. + :pypi:`pytest-pingguo-pytest-plugin` + *last release*: Oct 26, 2022, + *status*: 4 - Beta, + *requires*: N/A + + pingguo test + :pypi:`pytest-pings` *last release*: Jun 29, 2019, *status*: 3 - Alpha, @@ -5284,6 +7941,13 @@ This list contains 963 plugins. Pytest plugin for functional testing of data analysispipelines + :pypi:`pytest-pitch` + *last release*: Nov 02, 2023, + *status*: 4 - Beta, + *requires*: pytest >=7.3.1 + + runs tests in an order such that coverage increases as fast as possible + :pypi:`pytest-platform-markers` *last release*: Sep 09, 2019, *status*: 4 - Beta, @@ -5306,12 +7970,33 @@ This list contains 963 plugins. Pytest plugin for reading playbooks. :pypi:`pytest-playwright` - *last release*: Oct 28, 2021, + *last release*: Feb 02, 2024, *status*: N/A, - *requires*: pytest + *requires*: pytest (<9.0.0,>=6.2.4) A pytest wrapper with fixtures for Playwright to automate web browsers + :pypi:`pytest_playwright_async` + *last release*: Feb 25, 2024, + *status*: N/A, + *requires*: N/A + + ASYNC Pytest plugin for Playwright + + :pypi:`pytest-playwright-asyncio` + *last release*: Aug 29, 2023, + *status*: N/A, + *requires*: N/A + + + + :pypi:`pytest-playwright-enhanced` + *last release*: Mar 24, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + A pytest plugin for playwright python + :pypi:`pytest-playwrights` *last release*: Dec 02, 2021, *status*: N/A, @@ -5326,8 +8011,22 @@ This list contains 963 plugins. A pytest wrapper for snapshot testing with playwright + :pypi:`pytest-playwright-visual` + *last release*: Apr 28, 2022, + *status*: N/A, + *requires*: N/A + + A pytest fixture for visual testing with Playwright + + :pypi:`pytest-plone` + *last release*: Jan 05, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + Pytest plugin to test Plone addons + :pypi:`pytest-plt` - *last release*: Aug 17, 2020, + *last release*: Jan 17, 2024, *status*: 5 - Production/Stable, *requires*: pytest @@ -5341,9 +8040,9 @@ This list contains 963 plugins. A plugin to help developing and testing other plugins :pypi:`pytest-plus` - *last release*: Mar 19, 2020, + *last release*: Mar 26, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.50) + *requires*: pytest>=7.4.2 PyTest Plus Plugin :: extends pytest functionality @@ -5354,13 +8053,27 @@ This list contains 963 plugins. + :pypi:`pytest-pogo` + *last release*: Mar 11, 2024, + *status*: 1 - Planning, + *requires*: pytest (>=7,<9) + + Pytest plugin for pogo-migrate + :pypi:`pytest-pointers` - *last release*: Oct 14, 2021, + *last release*: Dec 26, 2022, *status*: N/A, *requires*: N/A Pytest plugin to define functions you test with special marks for better navigation and reports + :pypi:`pytest-pokie` + *last release*: Oct 19, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Pokie plugin for pytest + :pypi:`pytest-polarion-cfme` *last release*: Nov 13, 2017, *status*: 3 - Alpha, @@ -5403,13 +8116,27 @@ This list contains 963 plugins. Visualize your failed tests with poo + :pypi:`pytest-pook` + *last release*: Feb 15, 2024, + *status*: 4 - Beta, + *requires*: pytest + + Pytest plugin for pook + :pypi:`pytest-pop` - *last release*: Aug 19, 2021, + *last release*: May 09, 2023, *status*: 5 - Production/Stable, *requires*: pytest A pytest plugin to help with testing pop projects + :pypi:`pytest-porringer` + *last release*: Jan 18, 2024, + *status*: N/A, + *requires*: pytest>=7.4.4 + + + :pypi:`pytest-portion` *last release*: Jan 28, 2021, *status*: 4 - Beta, @@ -5425,9 +8152,9 @@ This list contains 963 plugins. Run PostgreSQL in Docker container in Pytest. :pypi:`pytest-postgresql` - *last release*: Nov 05, 2021, + *last release*: Mar 11, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest >=6.2 Postgresql fixtures and fixture factories for Pytest. @@ -5438,8 +8165,29 @@ This list contains 963 plugins. pytest plugin with powerful fixtures + :pypi:`pytest-powerpack` + *last release*: Mar 17, 2024, + *status*: N/A, + *requires*: pytest (>=8.1.1,<9.0.0) + + + + :pypi:`pytest-prefer-nested-dup-tests` + *last release*: Apr 27, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=7.1.1,<8.0.0) + + A Pytest plugin to drop duplicated tests during collection, but will prefer keeping nested packages. + + :pypi:`pytest-pretty` + *last release*: Apr 05, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest>=7 + + pytest plugin for printing summary data as I want it + :pypi:`pytest-pretty-terminal` - *last release*: Nov 24, 2021, + *last release*: Jan 31, 2022, *status*: N/A, *requires*: pytest (>=3.4.1) @@ -5453,12 +8201,33 @@ This list contains 963 plugins. Minitest-style test colors :pypi:`pytest-print` - *last release*: Jun 17, 2021, + *last release*: Aug 25, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6) + *requires*: pytest>=7.4 pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) + :pypi:`pytest-priority` + *last release*: Jul 23, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin for add priority for tests + + :pypi:`pytest-proceed` + *last release*: Apr 10, 2024, + *status*: N/A, + *requires*: pytest + + + + :pypi:`pytest-profiles` + *last release*: Dec 09, 2021, + *status*: 4 - Beta, + *requires*: pytest (>=3.7.0) + + pytest plugin for configuration profiles + :pypi:`pytest-profiling` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -5467,9 +8236,9 @@ This list contains 963 plugins. Profiling plugin for py.test :pypi:`pytest-progress` - *last release*: Nov 09, 2021, + *last release*: Jan 31, 2022, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.7) + *requires*: N/A pytest plugin for instant test progress status @@ -5480,6 +8249,13 @@ This list contains 963 plugins. Report test pass / failures to a Prometheus PushGateway + :pypi:`pytest-prometheus-pushgateway` + *last release*: Sep 27, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest report plugin for Zulip + :pypi:`pytest-prosper` *last release*: Sep 24, 2018, *status*: N/A, @@ -5487,6 +8263,13 @@ This list contains 963 plugins. Test helpers for Prosper projects + :pypi:`pytest-prysk` + *last release*: Mar 12, 2024, + *status*: 4 - Beta, + *requires*: pytest (>=7.3.2) + + Pytest plugin for prysk + :pypi:`pytest-pspec` *last release*: Jun 02, 2020, *status*: 4 - Beta, @@ -5502,7 +8285,7 @@ This list contains 963 plugins. pytest plugin for testing applications that use psqlgraph :pypi:`pytest-ptera` - *last release*: Oct 20, 2021, + *last release*: Mar 01, 2022, *status*: N/A, *requires*: pytest (>=6.2.4,<7.0.0) @@ -5515,6 +8298,13 @@ This list contains 963 plugins. Pytest PuDB debugger integration + :pypi:`pytest-pumpkin-spice` + *last release*: Sep 18, 2022, + *status*: 4 - Beta, + *requires*: N/A + + A pytest plugin that makes your test reporting pumpkin-spiced + :pypi:`pytest-purkinje` *last release*: Oct 28, 2017, *status*: 2 - Pre-Alpha, @@ -5522,6 +8312,20 @@ This list contains 963 plugins. py.test plugin for purkinje test runner + :pypi:`pytest-pusher` + *last release*: Jan 06, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=3.6) + + pytest plugin for push report to minio + + :pypi:`pytest-py125` + *last release*: Dec 03, 2022, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-pycharm` *last release*: Aug 13, 2020, *status*: 5 - Production/Stable, @@ -5530,7 +8334,7 @@ This list contains 963 plugins. Plugin for py.test to enter PyCharm debugger on uncaught exceptions :pypi:`pytest-pycodestyle` - *last release*: Aug 10, 2020, + *last release*: Oct 28, 2022, *status*: 3 - Alpha, *requires*: N/A @@ -5544,19 +8348,33 @@ This list contains 963 plugins. py.test plugin to connect to a remote debug server with PyDev or PyCharm. :pypi:`pytest-pydocstyle` - *last release*: Aug 10, 2020, + *last release*: Jan 05, 2023, *status*: 3 - Alpha, *requires*: N/A pytest plugin to run pydocstyle :pypi:`pytest-pylint` - *last release*: Nov 09, 2020, + *last release*: Oct 06, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.4) + *requires*: pytest >=7.0 pytest plugin to check source code with pylint + :pypi:`pytest-pymysql-autorecord` + *last release*: Sep 02, 2022, + *status*: N/A, + *requires*: N/A + + Record PyMySQL queries and mock with the stored data. + + :pypi:`pytest-pyodide` + *last release*: Dec 09, 2023, + *status*: N/A, + *requires*: pytest + + Pytest plugin for testing applications that use Pyodide + :pypi:`pytest-pypi` *last release*: Mar 04, 2018, *status*: 3 - Alpha, @@ -5572,11 +8390,11 @@ This list contains 963 plugins. Core engine for cookiecutter-qa and pytest-play packages :pypi:`pytest-pyppeteer` - *last release*: Feb 16, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=6.0.2) + *last release*: Apr 28, 2022, + *status*: N/A, + *requires*: pytest (>=6.2.5,<7.0.0) - A plugin to run pyppeteer in pytest. + A plugin to run pyppeteer in pytest :pypi:`pytest-pyq` *last release*: Mar 10, 2020, @@ -5586,7 +8404,7 @@ This list contains 963 plugins. Pytest fixture "q" for pyq :pypi:`pytest-pyramid` - *last release*: Oct 15, 2021, + *last release*: Oct 11, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -5599,13 +8417,34 @@ This list contains 963 plugins. Pyramid server fixture for py.test + :pypi:`pytest-pyreport` + *last release*: Feb 03, 2024, + *status*: N/A, + *requires*: pytest + + PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report + :pypi:`pytest-pyright` - *last release*: Aug 16, 2021, + *last release*: Jan 26, 2024, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest >=7.0.0 Pytest plugin for type checking code with Pyright + :pypi:`pytest-pyspec` + *last release*: Jan 02, 2024, + *status*: N/A, + *requires*: pytest (>=7.2.1,<8.0.0) + + A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it". + + :pypi:`pytest-pystack` + *last release*: Jan 04, 2024, + *status*: N/A, + *requires*: pytest >=3.5.0 + + Plugin to run pystack after a timeout for a test suite. + :pypi:`pytest-pytestrail` *last release*: Aug 27, 2020, *status*: 4 - Beta, @@ -5613,10 +8452,17 @@ This list contains 963 plugins. Pytest plugin for interaction with TestRail + :pypi:`pytest-pythonhashseed` + *last release*: Feb 25, 2024, + *status*: 4 - Beta, + *requires*: pytest>=3.0.0 + + Pytest plugin to set PYTHONHASHSEED env var. + :pypi:`pytest-pythonpath` - *last release*: Aug 22, 2018, + *last release*: Feb 10, 2022, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest (<7,>=2.5.2) pytest plugin for adding to the PYTHONPATH from command line or configs. @@ -5627,6 +8473,27 @@ This list contains 963 plugins. pytest plugin for a better developer experience when working with the PyTorch test suite + :pypi:`pytest-pyvenv` + *last release*: Feb 27, 2024, + *status*: N/A, + *requires*: pytest ; extra == 'test' + + A package for create venv in tests + + :pypi:`pytest-pyvista` + *last release*: Sep 29, 2023, + *status*: 4 - Beta, + *requires*: pytest>=3.5.0 + + Pytest-pyvista package + + :pypi:`pytest-qaseio` + *last release*: Sep 12, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.2.2,<8.0.0) + + Pytest plugin for Qase.io integration + :pypi:`pytest-qasync` *last release*: Jul 12, 2021, *status*: 4 - Beta, @@ -5635,16 +8502,16 @@ This list contains 963 plugins. Pytest support for qasync. :pypi:`pytest-qatouch` - *last release*: Jun 26, 2021, + *last release*: Feb 14, 2023, *status*: 4 - Beta, *requires*: pytest (>=6.2.0) Pytest plugin for uploading test results to your QA Touch Testrun. :pypi:`pytest-qgis` - *last release*: Nov 25, 2021, + *last release*: Nov 29, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.2.3) + *requires*: pytest >=6.0 A pytest plugin for testing QGIS python plugins @@ -5663,9 +8530,9 @@ This list contains 963 plugins. pytest plugin to generate test result QR codes :pypi:`pytest-qt` - *last release*: Jun 13, 2021, + *last release*: Feb 07, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest pytest support for PyQt and PySide applications @@ -5684,21 +8551,28 @@ This list contains 963 plugins. A plugin for pytest to manage expected test failures :pypi:`pytest-quickcheck` - *last release*: Nov 15, 2020, + *last release*: Nov 05, 2022, *status*: 4 - Beta, - *requires*: pytest (<6.0.0,>=4.0) + *requires*: pytest (>=4.0) pytest plugin to generate random data inspired by QuickCheck + :pypi:`pytest_quickify` + *last release*: Jun 14, 2019, + *status*: N/A, + *requires*: pytest + + Run test suites with pytest-quickify. + :pypi:`pytest-rabbitmq` - *last release*: Jun 02, 2021, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest (>=6.2) RabbitMQ process and client fixtures for pytest :pypi:`pytest-race` - *last release*: Nov 21, 2016, + *last release*: Jun 07, 2022, *status*: 4 - Beta, *requires*: N/A @@ -5711,8 +8585,15 @@ This list contains 963 plugins. pytest plugin to implement PEP712 + :pypi:`pytest-rail` + *last release*: May 02, 2022, + *status*: N/A, + *requires*: pytest (>=3.6) + + pytest plugin for creating TestRail runs and adding results + :pypi:`pytest-railflow-testrail-reporter` - *last release*: Dec 02, 2021, + *last release*: Jun 29, 2022, *status*: 5 - Production/Stable, *requires*: pytest @@ -5733,7 +8614,7 @@ This list contains 963 plugins. Simple pytest plugin to look for regex in Exceptions :pypi:`pytest-raisin` - *last release*: Jun 25, 2020, + *last release*: Feb 06, 2022, *status*: N/A, *requires*: pytest @@ -5747,7 +8628,7 @@ This list contains 963 plugins. py.test plugin to randomize tests :pypi:`pytest-randomly` - *last release*: Nov 30, 2021, + *last release*: Aug 15, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -5768,30 +8649,44 @@ This list contains 963 plugins. Randomise the order in which pytest tests are run with some control over the randomness :pypi:`pytest-random-order` - *last release*: Nov 30, 2018, + *last release*: Jan 20, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest >=3.0.0 Randomise the order in which pytest tests are run with some control over the randomness + :pypi:`pytest-ranking` + *last release*: Mar 18, 2024, + *status*: 4 - Beta, + *requires*: pytest >=7.4.3 + + A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection + :pypi:`pytest-readme` - *last release*: Dec 28, 2014, + *last release*: Sep 02, 2022, *status*: 5 - Production/Stable, *requires*: N/A Test your README.md file :pypi:`pytest-reana` - *last release*: Nov 22, 2021, + *last release*: Mar 14, 2024, *status*: 3 - Alpha, *requires*: N/A Pytest fixtures for REANA. + :pypi:`pytest-recorder` + *last release*: Nov 21, 2023, + *status*: N/A, + *requires*: N/A + + Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. + :pypi:`pytest-recording` - *last release*: Jul 08, 2021, + *last release*: Dec 06, 2023, *status*: 4 - Beta, - *requires*: pytest (>=3.5.0) + *requires*: pytest>=3.5.0 A pytest plugin that allows you recording of network interactions via VCR.py @@ -5803,14 +8698,14 @@ This list contains 963 plugins. Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal :pypi:`pytest-redis` - *last release*: Nov 03, 2021, + *last release*: Apr 19, 2023, *status*: 5 - Production/Stable, - *requires*: pytest + *requires*: pytest (>=6.2) Redis fixtures and fixture factories for Pytest. :pypi:`pytest-redislite` - *last release*: Sep 19, 2021, + *last release*: Apr 05, 2022, *status*: 4 - Beta, *requires*: pytest @@ -5837,19 +8732,33 @@ This list contains 963 plugins. Conveniently run pytest with a dot-formatted test reference. + :pypi:`pytest-regex` + *last release*: May 29, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + Select pytest tests with regular expressions + + :pypi:`pytest-regex-dependency` + *last release*: Jun 12, 2022, + *status*: N/A, + *requires*: pytest + + Management of Pytest dependencies via regex patterns + :pypi:`pytest-regressions` - *last release*: Jan 27, 2021, + *last release*: Aug 31, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.5.0) + *requires*: pytest >=6.2.0 Easy to use fixtures to write regression tests. :pypi:`pytest-regtest` - *last release*: Jun 03, 2021, + *last release*: Feb 26, 2024, *status*: N/A, - *requires*: N/A + *requires*: pytest>7.2 - pytest plugin for regression tests + pytest plugin for snapshot regression testing :pypi:`pytest-relative-order` *last release*: May 17, 2021, @@ -5859,9 +8768,9 @@ This list contains 963 plugins. a pytest plugin that sorts tests using "before" and "after" markers :pypi:`pytest-relaxed` - *last release*: Jun 14, 2019, + *last release*: Mar 29, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (<5,>=3) + *requires*: pytest>=7 Relaxed test discovery/organization for pytest @@ -5873,21 +8782,21 @@ This list contains 963 plugins. Pytest plugin to create a temporary directory with remote files :pypi:`pytest-remotedata` - *last release*: Jul 20, 2019, - *status*: 3 - Alpha, - *requires*: pytest (>=3.1) + *last release*: Sep 26, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest >=4.6 Pytest plugin for controlling remote data access. :pypi:`pytest-remote-response` - *last release*: Jun 30, 2021, - *status*: 4 - Beta, + *last release*: Apr 26, 2023, + *status*: 5 - Production/Stable, *requires*: pytest (>=4.6) Pytest plugin for capturing and mocking connection requests. :pypi:`pytest-remove-stale-bytecode` - *last release*: Mar 04, 2020, + *last release*: Jul 07, 2023, *status*: 4 - Beta, *requires*: pytest @@ -5901,21 +8810,28 @@ This list contains 963 plugins. Reorder tests depending on their paths and names. :pypi:`pytest-repeat` - *last release*: Oct 31, 2020, + *last release*: Oct 09, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.6) + *requires*: pytest pytest plugin for repeating tests + :pypi:`pytest_repeater` + *last release*: Feb 09, 2018, + *status*: 1 - Planning, + *requires*: N/A + + py.test plugin for repeating single test multiple times. + :pypi:`pytest-replay` - *last release*: Jun 09, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=3.0.0) + *last release*: Jan 11, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests :pypi:`pytest-repo-health` - *last release*: Nov 23, 2021, + *last release*: Apr 17, 2023, *status*: 3 - Alpha, *requires*: pytest @@ -5929,19 +8845,26 @@ This list contains 963 plugins. Creates json report that is compatible with atom.io's linter message format :pypi:`pytest-reporter` - *last release*: Jul 22, 2021, + *last release*: Feb 28, 2024, *status*: 4 - Beta, *requires*: pytest Generate Pytest reports with templates :pypi:`pytest-reporter-html1` - *last release*: Jun 08, 2021, + *last release*: Feb 28, 2024, *status*: 4 - Beta, *requires*: N/A A basic HTML report template for Pytest + :pypi:`pytest-reporter-html-dots` + *last release*: Jan 22, 2023, + *status*: N/A, + *requires*: N/A + + A basic HTML report for pytest using Jinja2 template engine. + :pypi:`pytest-reportinfra` *last release*: Aug 11, 2019, *status*: 3 - Alpha, @@ -5957,9 +8880,9 @@ This list contains 963 plugins. A plugin to report summarized results in a table format :pypi:`pytest-reportlog` - *last release*: Dec 11, 2020, + *last release*: May 22, 2023, *status*: 3 - Alpha, - *requires*: pytest (>=5.2) + *requires*: pytest Replacement for the --resultlog option, focused in simplicity and extensibility @@ -5978,12 +8901,26 @@ This list contains 963 plugins. pytest plugin for adding tests' parameters to junit report :pypi:`pytest-reportportal` - *last release*: Jun 18, 2021, + *last release*: Mar 27, 2024, *status*: N/A, - *requires*: pytest (>=3.8.0) + *requires*: pytest>=3.8.0 Agent for Reporting results of tests to the Report Portal + :pypi:`pytest-report-stream` + *last release*: Oct 22, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A pytest plugin which allows to stream test reports at runtime + + :pypi:`pytest-repo-structure` + *last release*: Mar 18, 2024, + *status*: 1 - Planning, + *requires*: N/A + + Pytest Repo Structure + :pypi:`pytest-reqs` *last release*: May 12, 2019, *status*: N/A, @@ -5998,8 +8935,29 @@ This list contains 963 plugins. A simple plugin to use with pytest + :pypi:`pytest-requestselapsed` + *last release*: Aug 14, 2022, + *status*: N/A, + *requires*: N/A + + collect and show http requests elapsed time + + :pypi:`pytest-requests-futures` + *last release*: Jul 06, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest Plugin to Mock Requests Futures + + :pypi:`pytest-requires` + *last release*: Dec 21, 2021, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A pytest plugin to elegantly skip tests with optional requirements + :pypi:`pytest-reraise` - *last release*: Jun 17, 2021, + *last release*: Sep 20, 2022, *status*: 5 - Production/Stable, *requires*: pytest (>=4.6) @@ -6012,19 +8970,47 @@ This list contains 963 plugins. Re-run only changed files in specified branch + :pypi:`pytest-rerun-all` + *last release*: Nov 16, 2023, + *status*: 3 - Alpha, + *requires*: pytest (>=7.0.0) + + Rerun testsuite for a certain time or iterations + + :pypi:`pytest-rerunclassfailures` + *last release*: Mar 29, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.2 + + pytest rerun class failures plugin + :pypi:`pytest-rerunfailures` - *last release*: Sep 17, 2021, + *last release*: Mar 13, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.3) + *requires*: pytest >=7.2 pytest plugin to re-run tests to eliminate flaky failures + :pypi:`pytest-rerunfailures-all-logs` + *last release*: Mar 07, 2022, + *status*: 5 - Production/Stable, + *requires*: N/A + + pytest plugin to re-run tests to eliminate flaky failures + + :pypi:`pytest-reserial` + *last release*: Feb 08, 2024, + *status*: 4 - Beta, + *requires*: pytest + + Pytest fixture for recording and replaying serial port traffic. + :pypi:`pytest-resilient-circuits` - *last release*: Nov 15, 2021, + *last release*: Apr 03, 2024, *status*: N/A, - *requires*: N/A + *requires*: pytest~=4.6; python_version == "2.7" - Resilient Circuits fixtures for PyTest. + Resilient Circuits fixtures for PyTest :pypi:`pytest-resource` *last release*: Nov 14, 2018, @@ -6040,27 +9026,62 @@ This list contains 963 plugins. Provides path for uniform access to test resources in isolated directory + :pypi:`pytest-resource-usage` + *last release*: Nov 06, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.0.0 + + Pytest plugin for reporting running time and peak memory usage + :pypi:`pytest-responsemock` - *last release*: Oct 10, 2020, + *last release*: Mar 10, 2022, *status*: 5 - Production/Stable, *requires*: N/A Simplified requests calls mocking for pytest :pypi:`pytest-responses` - *last release*: Apr 26, 2021, + *last release*: Oct 11, 2022, *status*: N/A, *requires*: pytest (>=2.5) py.test integration for responses + :pypi:`pytest-rest-api` + *last release*: Aug 08, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.2,<8.0.0) + + + :pypi:`pytest-restrict` - *last release*: Aug 12, 2021, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest Pytest plugin to restrict the test types allowed + :pypi:`pytest-result-log` + *last release*: Jan 10, 2024, + *status*: N/A, + *requires*: pytest>=7.2.0 + + A pytest plugin that records the start, end, and result information of each use case in a log file + + :pypi:`pytest-result-sender` + *last release*: Apr 20, 2023, + *status*: N/A, + *requires*: pytest>=7.3.1 + + + + :pypi:`pytest-resume` + *last release*: Apr 22, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0) + + A Pytest plugin to resuming from the last run test + :pypi:`pytest-rethinkdb` *last release*: Jul 24, 2016, *status*: 4 - Beta, @@ -6068,13 +9089,62 @@ This list contains 963 plugins. A RethinkDB plugin for pytest. + :pypi:`pytest-retry` + *last release*: Feb 04, 2024, + *status*: N/A, + *requires*: pytest >=7.0.0 + + Adds the ability to retry flaky tests in CI environments + + :pypi:`pytest-retry-class` + *last release*: Mar 25, 2023, + *status*: N/A, + *requires*: pytest (>=5.3) + + A pytest plugin to rerun entire class on failure + + :pypi:`pytest-reusable-testcases` + *last release*: Apr 28, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-reverse` - *last release*: Aug 12, 2021, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest Pytest plugin to reverse test order. + :pypi:`pytest-rich` + *last release*: Mar 03, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=7.0) + + Leverage rich for richer test session output + + :pypi:`pytest-richer` + *last release*: Oct 27, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + Pytest plugin providing a Rich based reporter. + + :pypi:`pytest-rich-reporter` + *last release*: Feb 17, 2022, + *status*: 1 - Planning, + *requires*: pytest (>=5.0.0) + + A pytest plugin using Rich for beautiful test result formatting. + + :pypi:`pytest-richtrace` + *last release*: Jun 20, 2023, + *status*: N/A, + *requires*: N/A + + A pytest plugin that displays the names and information of the pytest hook functions as they are executed. + :pypi:`pytest-ringo` *last release*: Sep 27, 2017, *status*: 3 - Alpha, @@ -6082,6 +9152,13 @@ This list contains 963 plugins. pytest plugin to test webapplications using the Ringo webframework + :pypi:`pytest-rmsis` + *last release*: Aug 10, 2022, + *status*: N/A, + *requires*: pytest (>=5.3.5) + + Sycronise pytest results to Jira RMsis + :pypi:`pytest-rng` *last release*: Aug 08, 2019, *status*: 5 - Production/Stable, @@ -6090,12 +9167,19 @@ This list contains 963 plugins. Fixtures for seeding tests and making randomness reproducible :pypi:`pytest-roast` - *last release*: Jul 29, 2021, + *last release*: Nov 09, 2022, *status*: 5 - Production/Stable, *requires*: pytest pytest plugin for ROAST configuration override and fixtures + :pypi:`pytest_robotframework` + *last release*: Mar 29, 2024, + *status*: N/A, + *requires*: pytest<9,>=7 + + a pytest plugin that can run both python and robotframework tests while generating robot reports for them + :pypi:`pytest-rocketchat` *last release*: Apr 18, 2021, *status*: 5 - Production/Stable, @@ -6118,14 +9202,14 @@ This list contains 963 plugins. Extend py.test for RPC OpenStack testing. :pypi:`pytest-rst` - *last release*: Sep 21, 2021, + *last release*: Jan 26, 2023, *status*: N/A, - *requires*: pytest + *requires*: N/A Test code from RST documents with pytest :pypi:`pytest-rt` - *last release*: Sep 04, 2021, + *last release*: May 05, 2022, *status*: N/A, *requires*: N/A @@ -6138,6 +9222,13 @@ This list contains 963 plugins. Coverage-based regression test selection (RTS) plugin for pytest + :pypi:`pytest-ruff` + *last release*: Mar 10, 2024, + *status*: 4 - Beta, + *requires*: pytest (>=5) + + pytest plugin to check ruff requirements. + :pypi:`pytest-run-changed` *last release*: Apr 02, 2021, *status*: 3 - Alpha, @@ -6152,20 +9243,41 @@ This list contains 963 plugins. implement a --failed option for pytest - :pypi:`pytest-runner` - *last release*: May 19, 2021, + :pypi:`pytest-run-subprocess` + *last release*: Nov 12, 2022, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.6) ; extra == 'testing' + *requires*: pytest + + Pytest Plugin for running and testing subprocesses. + + :pypi:`pytest-runtime-types` + *last release*: Feb 09, 2023, + *status*: N/A, + *requires*: pytest - Invoke py.test as distutils command with dependency resolution + Checks type annotations on runtime while running tests. :pypi:`pytest-runtime-xfail` *last release*: Aug 26, 2021, *status*: N/A, - *requires*: N/A + *requires*: pytest>=5.0.0 Call runtime_xfail() to mark running test as xfail. + :pypi:`pytest-runtime-yoyo` + *last release*: Jun 12, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0) + + run case mark timeout + + :pypi:`pytest-saccharin` + *last release*: Oct 31, 2022, + *status*: 3 - Alpha, + *requires*: N/A + + pytest-saccharin is a updated fork of pytest-sugar, a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). + :pypi:`pytest-salt` *last release*: Jan 27, 2020, *status*: 4 - Beta, @@ -6181,9 +9293,9 @@ This list contains 963 plugins. A Pytest plugin that builds and creates docker containers :pypi:`pytest-salt-factories` - *last release*: Sep 16, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=6.0.0) + *last release*: Mar 22, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.0.0 Pytest Salt Plugin @@ -6222,8 +9334,15 @@ This list contains 963 plugins. + :pypi:`pytest_sauce` + *last release*: Jul 14, 2014, + *status*: 3 - Alpha, + *requires*: N/A + + pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs + :pypi:`pytest-sbase` - *last release*: Dec 03, 2021, + *last release*: Apr 14, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -6236,13 +9355,27 @@ This list contains 963 plugins. pytest plugin for test scenarios + :pypi:`pytest-schedule` + *last release*: Jan 07, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + The job of test scheduling for humans. + :pypi:`pytest-schema` - *last release*: Aug 31, 2020, + *last release*: Feb 16, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.5.0) + *requires*: pytest >=3.5.0 👍 Validate return values against a schema-like object in testing + :pypi:`pytest-screenshot-on-failure` + *last release*: Jul 21, 2023, + *status*: 4 - Beta, + *requires*: N/A + + Saves a screenshot when a test case from a pytest execution fails + :pypi:`pytest-securestore` *last release*: Nov 08, 2021, *status*: 4 - Beta, @@ -6258,21 +9391,28 @@ This list contains 963 plugins. A pytest plugin which allows to (de-)select tests from a file. :pypi:`pytest-selenium` - *last release*: Sep 19, 2020, + *last release*: Feb 01, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.0.0) + *requires*: pytest>=6.0.0 pytest plugin for Selenium + :pypi:`pytest-selenium-auto` + *last release*: Nov 07, 2023, + *status*: N/A, + *requires*: pytest >= 7.0.0 + + pytest plugin to automatically capture screenshots upon selenium webdriver events + :pypi:`pytest-seleniumbase` - *last release*: Dec 03, 2021, + *last release*: Apr 14, 2024, *status*: 5 - Production/Stable, *requires*: N/A A complete web automation framework for end-to-end testing. :pypi:`pytest-selenium-enhancer` - *last release*: Nov 26, 2020, + *last release*: Apr 29, 2022, *status*: 5 - Production/Stable, *requires*: N/A @@ -6285,6 +9425,13 @@ This list contains 963 plugins. A pytest package implementing perceptualdiff for Selenium tests. + :pypi:`pytest-selfie` + *last release*: Apr 05, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + A pytest plugin for selfie snapshot testing. + :pypi:`pytest-send-email` *last release*: Dec 04, 2019, *status*: N/A, @@ -6293,26 +9440,40 @@ This list contains 963 plugins. Send pytest execution result email :pypi:`pytest-sentry` - *last release*: Apr 21, 2021, + *last release*: Apr 05, 2024, *status*: N/A, *requires*: pytest A pytest plugin to send testrun information to Sentry.io + :pypi:`pytest-sequence-markers` + *last release*: May 23, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Pytest plugin for sequencing markers for execution of tests + :pypi:`pytest-server-fixtures` - *last release*: May 28, 2019, + *last release*: Dec 19, 2023, *status*: 5 - Production/Stable, *requires*: pytest Extensible server fixures for py.test :pypi:`pytest-serverless` - *last release*: Nov 27, 2021, + *last release*: May 09, 2022, *status*: 4 - Beta, *requires*: N/A Automatically mocks resources from serverless.yml in pytest using moto. + :pypi:`pytest-servers` + *last release*: Mar 19, 2024, + *status*: 3 - Alpha, + *requires*: pytest>=6.2 + + pytest servers + :pypi:`pytest-services` *last release*: Oct 30, 2020, *status*: 6 - Mature, @@ -6341,6 +9502,13 @@ This list contains 963 plugins. pytest-session_to_file is a py.test plugin for capturing and saving to file the stdout of py.test. + :pypi:`pytest-setupinfo` + *last release*: Jan 23, 2023, + *status*: N/A, + *requires*: N/A + + Displaying setup info during pytest command run + :pypi:`pytest-sftpserver` *last release*: Sep 16, 2019, *status*: 4 - Beta, @@ -6355,13 +9523,34 @@ This list contains 963 plugins. + :pypi:`pytest-share-hdf` + *last release*: Sep 21, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + Plugin to save test data in HDF files and retrieve them for comparison + + :pypi:`pytest-sharkreport` + *last release*: Jul 11, 2022, + *status*: N/A, + *requires*: pytest (>=3.5) + + this is pytest report plugin. + :pypi:`pytest-shell` - *last release*: Nov 07, 2021, + *last release*: Mar 27, 2022, *status*: N/A, *requires*: N/A A pytest plugin to help with testing shell scripts / black box commands + :pypi:`pytest-shell-utilities` + *last release*: Feb 23, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=7.4.0 + + Pytest plugin to simplify running shell commands against the system + :pypi:`pytest-sheraf` *last release*: Feb 11, 2020, *status*: N/A, @@ -6370,9 +9559,9 @@ This list contains 963 plugins. Versatile ZODB abstraction layer - pytest fixtures :pypi:`pytest-sherlock` - *last release*: Nov 18, 2021, + *last release*: Aug 14, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.5.1) + *requires*: pytest >=3.5.1 pytest plugin help to find coupled tests @@ -6390,6 +9579,13 @@ This list contains 963 plugins. A goodie-bag of unix shell and environment tools for py.test + :pypi:`pytest-simbind` + *last release*: Mar 28, 2024, + *status*: N/A, + *requires*: pytest>=7.0.0 + + Pytest plugin to operate with objects generated by Simbind tool. + :pypi:`pytest-simplehttpserver` *last release*: Jun 24, 2021, *status*: 4 - Beta, @@ -6419,9 +9615,9 @@ This list contains 963 plugins. Allow for multiple processes to log to a single file :pypi:`pytest-skip-markers` - *last release*: Oct 04, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=6.0.0) + *last release*: Jan 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest >=7.1.0 Pytest Salt Plugin @@ -6440,12 +9636,19 @@ This list contains 963 plugins. Automatically skip tests that don't need to run! :pypi:`pytest-skip-slow` - *last release*: Sep 28, 2021, + *last release*: Feb 09, 2023, *status*: N/A, - *requires*: N/A + *requires*: pytest>=6.2.0 A pytest plugin to skip \`@pytest.mark.slow\` tests by default. + :pypi:`pytest-skipuntil` + *last release*: Nov 25, 2023, + *status*: 4 - Beta, + *requires*: pytest >=3.8.0 + + A simple pytest plugin to skip flapping test with deadline + :pypi:`pytest-slack` *last release*: Dec 15, 2020, *status*: 5 - Production/Stable, @@ -6460,6 +9663,27 @@ This list contains 963 plugins. A pytest plugin to skip \`@pytest.mark.slow\` tests by default. + :pypi:`pytest-slowest-first` + *last release*: Dec 11, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Sort tests by their last duration, slowest first + + :pypi:`pytest-slow-first` + *last release*: Jan 30, 2024, + *status*: 4 - Beta, + *requires*: pytest >=3.5.0 + + Prioritize running the slowest tests first. + + :pypi:`pytest-slow-last` + *last release*: Dec 10, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + Run tests in order of execution time (faster tests first) + :pypi:`pytest-smartcollect` *last release*: Oct 04, 2018, *status*: N/A, @@ -6474,6 +9698,13 @@ This list contains 963 plugins. Smart coverage plugin for pytest. + :pypi:`pytest-smell` + *last release*: Jun 26, 2022, + *status*: N/A, + *requires*: N/A + + Automated bad smell detection tool for Pytest + :pypi:`pytest-smtp` *last release*: Feb 20, 2021, *status*: N/A, @@ -6481,6 +9712,27 @@ This list contains 963 plugins. Send email with pytest execution result + :pypi:`pytest-smtp4dev` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Plugin for smtp4dev API + + :pypi:`pytest-smtpd` + *last release*: May 15, 2023, + *status*: N/A, + *requires*: pytest + + An SMTP server for testing built on aiosmtpd + + :pypi:`pytest-smtp-test-server` + *last release*: Dec 03, 2023, + *status*: 2 - Pre-Alpha, + *requires*: pytest (>=7.4.3,<8.0.0) + + pytest plugin for using \`smtp-test-server\` as a fixture + :pypi:`pytest-snail` *last release*: Nov 04, 2019, *status*: 3 - Alpha, @@ -6496,7 +9748,14 @@ This list contains 963 plugins. py.test plugin for Snap-CI :pypi:`pytest-snapshot` - *last release*: Dec 02, 2021, + *last release*: Apr 23, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.0.0) + + A plugin for snapshot testing with pytest. + + :pypi:`pytest-snapshot-with-message-generator` + *last release*: Jul 25, 2023, *status*: 4 - Beta, *requires*: pytest (>=3.0.0) @@ -6509,13 +9768,27 @@ This list contains 963 plugins. + :pypi:`pytest-snowflake-bdd` + *last release*: Jan 05, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=6.2.0) + + Setup test data and run tests on snowflake in BDD style! + :pypi:`pytest-socket` - *last release*: Aug 28, 2021, + *last release*: Jan 28, 2024, *status*: 4 - Beta, - *requires*: pytest (>=3.6.3) + *requires*: pytest (>=6.2.5) Pytest Plugin to disable socket calls during tests + :pypi:`pytest-sofaepione` + *last release*: Aug 17, 2022, + *status*: N/A, + *requires*: N/A + + Test the installation of SOFA and the SofaEpione plugin. + :pypi:`pytest-soft-assertions` *last release*: May 05, 2020, *status*: 3 - Alpha, @@ -6523,6 +9796,13 @@ This list contains 963 plugins. + :pypi:`pytest-solidity` + *last release*: Jan 15, 2022, + *status*: 1 - Planning, + *requires*: pytest (<7,>=6.0.1) ; extra == 'tests' + + A PyTest library plugin for Solidity language. + :pypi:`pytest-solr` *last release*: May 11, 2020, *status*: 3 - Alpha, @@ -6530,6 +9810,13 @@ This list contains 963 plugins. Solr process and client fixtures for py.test. + :pypi:`pytest-sort` + *last release*: Jan 07, 2024, + *status*: N/A, + *requires*: pytest >=7.4.0 + + Tools for sorting test cases + :pypi:`pytest-sorter` *last release*: Apr 20, 2021, *status*: 4 - Beta, @@ -6537,6 +9824,13 @@ This list contains 963 plugins. A simple plugin to first execute tests that historically failed more + :pypi:`pytest-sosu` + *last release*: Aug 04, 2023, + *status*: 2 - Pre-Alpha, + *requires*: pytest + + Unofficial PyTest plugin for Sauce Labs + :pypi:`pytest-sourceorder` *last release*: Sep 01, 2021, *status*: 4 - Beta, @@ -6565,31 +9859,59 @@ This list contains 963 plugins. Library pytest-spec is a pytest plugin to display test execution output like a SPECIFICATION. + :pypi:`pytest-spec2md` + *last release*: Apr 10, 2024, + *status*: N/A, + *requires*: pytest>7.0 + + Library pytest-spec2md is a pytest plugin to create a markdown specification while running pytest. + + :pypi:`pytest-speed` + *last release*: Jan 22, 2023, + *status*: 3 - Alpha, + *requires*: pytest>=7 + + Modern benchmarking library for python with pytest integration. + :pypi:`pytest-sphinx` - *last release*: Aug 05, 2020, + *last release*: Apr 13, 2024, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest>=8.1.1 Doctest plugin for pytest with support for Sphinx-specific doctest-directives :pypi:`pytest-spiratest` - *last release*: Oct 13, 2021, + *last release*: Jan 01, 2024, *status*: N/A, *requires*: N/A - Exports unit tests as test runs in SpiraTest/Team/Plan + Exports unit tests as test runs in Spira (SpiraTest/Team/Plan) :pypi:`pytest-splinter` - *last release*: Dec 25, 2020, + *last release*: Sep 09, 2022, *status*: 6 - Mature, - *requires*: N/A + *requires*: pytest (>=3.0.0) Splinter plugin for pytest testing framework + :pypi:`pytest-splinter4` + *last release*: Feb 01, 2024, + *status*: 6 - Mature, + *requires*: pytest >=8.0.0 + + Pytest plugin for the splinter automation library + :pypi:`pytest-split` - *last release*: Nov 09, 2021, + *last release*: Jan 29, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5,<7) + *requires*: pytest (>=5,<9) + + Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. + + :pypi:`pytest-split-ext` + *last release*: Sep 23, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=5,<8) Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. @@ -6615,14 +9937,14 @@ This list contains 963 plugins. :pypi:`pytest-splunk-addon` - *last release*: Nov 29, 2021, + *last release*: Apr 19, 2024, *status*: N/A, - *requires*: pytest (>5.4.0,<6.3) + *requires*: pytest (>5.4.0,<8) A Dynamic test tool for Splunk Apps and Add-ons :pypi:`pytest-splunk-addon-ui-smartx` - *last release*: Oct 07, 2021, + *last release*: Mar 26, 2024, *status*: N/A, *requires*: N/A @@ -6649,6 +9971,20 @@ This list contains 963 plugins. pytest plugin with sqlalchemy related fixtures + :pypi:`pytest-sqlalchemy-mock` + *last release*: Mar 15, 2023, + *status*: 3 - Alpha, + *requires*: pytest (>=2.0) + + pytest sqlalchemy plugin for mock + + :pypi:`pytest-sqlalchemy-session` + *last release*: May 19, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0) + + A pytest plugin for preserving test isolation that use SQLAlchemy. + :pypi:`pytest-sql-bigquery` *last release*: Dec 19, 2019, *status*: N/A, @@ -6656,10 +9992,24 @@ This list contains 963 plugins. Yet another SQL-testing framework for BigQuery provided by pytest plugin + :pypi:`pytest-sqlfluff` + *last release*: Dec 21, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=3.5.0) + + A pytest plugin to use sqlfluff to enable format checking of sql files. + + :pypi:`pytest-squadcast` + *last release*: Feb 22, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest report plugin for Squadcast + :pypi:`pytest-srcpaths` *last release*: Oct 15, 2021, *status*: N/A, - *requires*: N/A + *requires*: pytest>=6.2.0 Add paths to sys.path @@ -6677,6 +10027,20 @@ This list contains 963 plugins. Start pytest run from a given point + :pypi:`pytest-star-track-issue` + *last release*: Feb 20, 2024, + *status*: N/A, + *requires*: N/A + + A package to prevent Dependency Confusion attacks against Yandex. + + :pypi:`pytest-static` + *last release*: Jan 15, 2024, + *status*: 1 - Planning, + *requires*: pytest (>=7.4.3,<8.0.0) + + pytest-static + :pypi:`pytest-statsd` *last release*: Nov 30, 2018, *status*: 5 - Production/Stable, @@ -6705,6 +10069,13 @@ This list contains 963 plugins. Run a test suite one failing test at a time. + :pypi:`pytest-stf` + *last release*: Mar 25, 2024, + *status*: N/A, + *requires*: pytest>=5.0 + + pytest plugin for openSTF + :pypi:`pytest-stoq` *last release*: Feb 09, 2021, *status*: 4 - Beta, @@ -6712,6 +10083,13 @@ This list contains 963 plugins. A plugin to pytest stoq + :pypi:`pytest-store` + *last release*: Nov 16, 2023, + *status*: 3 - Alpha, + *requires*: pytest (>=7.0.0) + + Pytest plugin to store values from test runs + :pypi:`pytest-stress` *last release*: Dec 07, 2019, *status*: 4 - Beta, @@ -6720,7 +10098,7 @@ This list contains 963 plugins. A Pytest plugin that allows you to loop tests for a user defined amount of time. :pypi:`pytest-structlog` - *last release*: Sep 21, 2021, + *last release*: Mar 13, 2024, *status*: N/A, *requires*: pytest @@ -6754,54 +10132,68 @@ This list contains 963 plugins. A pytest plugin to organize long run tests (named studies) without interfering the regular tests + :pypi:`pytest-subinterpreter` + *last release*: Nov 25, 2023, + *status*: N/A, + *requires*: pytest>=7.0.0 + + Run pytest in a subinterpreter + :pypi:`pytest-subprocess` - *last release*: Nov 07, 2021, + *last release*: Jan 28, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=4.0.0) A plugin to fake subprocess for pytest :pypi:`pytest-subtesthack` - *last release*: Mar 02, 2021, + *last release*: Jul 16, 2022, *status*: N/A, *requires*: N/A A hack to explicitly set up and tear down fixtures. :pypi:`pytest-subtests` - *last release*: May 29, 2021, + *last release*: Mar 07, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5.3.0) + *requires*: pytest >=7.0 unittest subTest() support and subtests fixture :pypi:`pytest-subunit` - *last release*: Aug 29, 2017, + *last release*: Sep 17, 2023, *status*: N/A, - *requires*: N/A + *requires*: pytest (>=2.3) pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. :pypi:`pytest-sugar` - *last release*: Jul 06, 2020, - *status*: 3 - Alpha, - *requires*: N/A + *last release*: Feb 01, 2024, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). - :pypi:`pytest-sugar-bugfix159` - *last release*: Nov 07, 2018, - *status*: 5 - Production/Stable, - *requires*: pytest (!=3.7.3,>=3.5); extra == 'testing' + :pypi:`pytest-suitemanager` + *last release*: Apr 28, 2023, + *status*: 4 - Beta, + *requires*: N/A - Workaround for https://github.com/Frozenball/pytest-sugar/issues/159 + A simple plugin to use with pytest - :pypi:`pytest-super-check` - *last release*: Aug 12, 2021, - *status*: 5 - Production/Stable, - *requires*: pytest + :pypi:`pytest-suite-timeout` + *last release*: Jan 26, 2024, + *status*: N/A, + *requires*: pytest>=7.0.0 + + A pytest plugin for ensuring max suite time + + :pypi:`pytest-supercov` + *last release*: Jul 02, 2023, + *status*: N/A, + *requires*: N/A - Pytest plugin to check your TestCase classes call super in setUp, tearDown, etc. + Pytest plugin for measuring explicit test-file to source-file coverage :pypi:`pytest-svn` *last release*: May 28, 2019, @@ -6817,8 +10209,36 @@ This list contains 963 plugins. pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. + :pypi:`pytest-synodic` + *last release*: Mar 09, 2024, + *status*: N/A, + *requires*: pytest>=8.0.2 + + Synodic Pytest utilities + + :pypi:`pytest-system-statistics` + *last release*: Feb 16, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest (>=6.0.0) + + Pytest plugin to track and report system usage statistics + + :pypi:`pytest-system-test-plugin` + *last release*: Feb 03, 2022, + *status*: N/A, + *requires*: N/A + + Pyst - Pytest System-Test Plugin + + :pypi:`pytest_tagging` + *last release*: Apr 08, 2024, + *status*: N/A, + *requires*: pytest<8.0.0,>=7.1.3 + + a pytest plugin to tag tests + :pypi:`pytest-takeltest` - *last release*: Oct 13, 2021, + *last release*: Feb 15, 2023, *status*: N/A, *requires*: N/A @@ -6831,8 +10251,15 @@ This list contains 963 plugins. + :pypi:`pytest-tally` + *last release*: May 22, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=6.2.5) + + A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. + :pypi:`pytest-tap` - *last release*: Oct 27, 2021, + *last release*: Jul 15, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=3.0) @@ -6859,6 +10286,20 @@ This list contains 963 plugins. tblineinfo is a py.test plugin that insert the node id in the final py.test report when --tb=line option is used + :pypi:`pytest-tcpclient` + *last release*: Nov 16, 2022, + *status*: N/A, + *requires*: pytest (<8,>=7.1.3) + + A pytest plugin for testing TCP clients + + :pypi:`pytest-tdd` + *last release*: Aug 18, 2023, + *status*: 4 - Beta, + *requires*: N/A + + run pytest on a python module + :pypi:`pytest-teamcity-logblock` *last release*: May 15, 2018, *status*: 4 - Beta, @@ -6873,6 +10314,13 @@ This list contains 963 plugins. Pytest to Telegram reporting plugin + :pypi:`pytest-telegram-notifier` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Telegram notification plugin for Pytest + :pypi:`pytest-tempdir` *last release*: Oct 11, 2019, *status*: 4 - Beta, @@ -6880,8 +10328,15 @@ This list contains 963 plugins. Predictable and repeatable tempdir support. + :pypi:`pytest-terra-fixt` + *last release*: Sep 15, 2022, + *status*: N/A, + *requires*: pytest (==6.2.5) + + Terraform and Terragrunt fixtures for pytest + :pypi:`pytest-terraform` - *last release*: Nov 10, 2021, + *last release*: Jun 20, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -6909,19 +10364,26 @@ This list contains 963 plugins. Test configuration plugin for pytest. :pypi:`pytest-testdirectory` - *last release*: Nov 06, 2018, + *last release*: May 02, 2023, *status*: 5 - Production/Stable, *requires*: pytest A py.test plugin providing temporary directories in unit tests. :pypi:`pytest-testdox` - *last release*: Oct 13, 2020, + *last release*: Jul 22, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.7.0) + *requires*: pytest (>=4.6.0) A testdox format reporter for pytest + :pypi:`pytest-test-grouping` + *last release*: Feb 01, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=2.5) + + A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. + :pypi:`pytest-test-groups` *last release*: Oct 25, 2016, *status*: 5 - Production/Stable, @@ -6930,9 +10392,23 @@ This list contains 963 plugins. A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. :pypi:`pytest-testinfra` - *last release*: Jun 20, 2021, + *last release*: Feb 15, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (!=3.0.2) + *requires*: pytest >=6 + + Test infrastructures + + :pypi:`pytest-testinfra-jpic` + *last release*: Sep 21, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Test infrastructures + + :pypi:`pytest-testinfra-winrm-transport` + *last release*: Sep 21, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A Test infrastructures @@ -6944,9 +10420,30 @@ This list contains 963 plugins. pytest reporting plugin for testlink :pypi:`pytest-testmon` - *last release*: Oct 22, 2021, + *last release*: Feb 27, 2024, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest <9,>=5 + + selects tests affected by changed files and methods + + :pypi:`pytest-testmon-dev` + *last release*: Mar 30, 2023, + *status*: 4 - Beta, + *requires*: pytest (<8,>=5) + + selects tests affected by changed files and methods + + :pypi:`pytest-testmon-oc` + *last release*: Jun 01, 2022, + *status*: 4 - Beta, + *requires*: pytest (<8,>=5) + + nOly selects tests affected by changed files and methods + + :pypi:`pytest-testmon-skip-libraries` + *last release*: Mar 03, 2023, + *status*: 4 - Beta, + *requires*: pytest (<8,>=5) selects tests affected by changed files and methods @@ -6957,6 +10454,13 @@ This list contains 963 plugins. Plugin to use TestObject Suites with Pytest + :pypi:`pytest-testpluggy` + *last release*: Jan 07, 2022, + *status*: N/A, + *requires*: pytest + + set your encoding + :pypi:`pytest-testrail` *last release*: Aug 27, 2020, *status*: N/A, @@ -6965,21 +10469,14 @@ This list contains 963 plugins. pytest plugin for creating TestRail runs and adding results :pypi:`pytest-testrail2` - *last release*: Nov 17, 2020, - *status*: N/A, - *requires*: pytest (>=5) - - A small example package - - :pypi:`pytest-testrail-api` - *last release*: Nov 30, 2021, + *last release*: Feb 10, 2023, *status*: N/A, - *requires*: pytest (>=5.5) + *requires*: pytest (<8.0,>=7.2.0) - Плагин Pytest, для интеграции с TestRail + A pytest plugin to upload results to TestRail. :pypi:`pytest-testrail-api-client` - *last release*: Dec 03, 2021, + *last release*: Dec 14, 2021, *status*: N/A, *requires*: pytest @@ -7006,10 +10503,17 @@ This list contains 963 plugins. pytest plugin for creating TestRail runs and adding results + :pypi:`pytest-testrail-integrator` + *last release*: Aug 01, 2022, + *status*: N/A, + *requires*: pytest (>=6.2.5) + + Pytest plugin for sending report to testrail system. + :pypi:`pytest-testrail-ns` - *last release*: Oct 08, 2021, + *last release*: Aug 12, 2022, *status*: N/A, - *requires*: pytest (>=3.6) + *requires*: N/A pytest plugin for creating TestRail runs and adding results @@ -7027,13 +10531,27 @@ This list contains 963 plugins. + :pypi:`pytest-testrail-results` + *last release*: Mar 04, 2024, + *status*: N/A, + *requires*: pytest >=7.2.0 + + A pytest plugin to upload results to TestRail. + :pypi:`pytest-testreport` - *last release*: Nov 12, 2021, + *last release*: Dec 01, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) + :pypi:`pytest-testreport-new` + *last release*: Oct 07, 2023, + *status*: 4 - Beta, + *requires*: pytest >=3.5.0 + + + :pypi:`pytest-testslide` *last release*: Jan 07, 2021, *status*: 5 - Production/Stable, @@ -7049,19 +10567,26 @@ This list contains 963 plugins. Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply :pypi:`pytest-test-utils` - *last release*: Nov 30, 2021, + *last release*: Feb 08, 2024, *status*: N/A, - *requires*: pytest (>=5) + *requires*: pytest >=3.9 :pypi:`pytest-tesults` - *last release*: Jul 31, 2021, + *last release*: Feb 15, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.5.0) + *requires*: pytest >=3.5.0 Tesults plugin for pytest + :pypi:`pytest-textual-snapshot` + *last release*: Aug 23, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + Snapshot testing for Textual apps + :pypi:`pytest-tezos` *last release*: Jan 16, 2020, *status*: 4 - Beta, @@ -7069,6 +10594,13 @@ This list contains 963 plugins. pytest-ligo + :pypi:`pytest-th2-bdd` + *last release*: May 13, 2022, + *status*: N/A, + *requires*: N/A + + pytest_th2_bdd + :pypi:`pytest-thawgun` *last release*: May 26, 2020, *status*: 3 - Alpha, @@ -7076,10 +10608,17 @@ This list contains 963 plugins. Pytest plugin for time travel + :pypi:`pytest-thread` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-threadleak` - *last release*: Sep 08, 2017, + *last release*: Jul 03, 2022, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest (>=3.1.1) Detects thread leaks @@ -7090,6 +10629,20 @@ This list contains 963 plugins. Ticking on tests + :pypi:`pytest-time` + *last release*: Jun 24, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + + + :pypi:`pytest-timeassert-ethan` + *last release*: Dec 25, 2023, + *status*: N/A, + *requires*: pytest + + execution duration + :pypi:`pytest-timeit` *last release*: Oct 13, 2016, *status*: 4 - Beta, @@ -7098,9 +10651,9 @@ This list contains 963 plugins. A pytest plugin to time test function runs :pypi:`pytest-timeout` - *last release*: Oct 11, 2021, + *last release*: Mar 07, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.0.0) + *requires*: pytest >=7.0.0 pytest plugin to abort hanging tests @@ -7112,35 +10665,56 @@ This list contains 963 plugins. Linux-only Pytest plugin to control durations of various test case execution phases :pypi:`pytest-timer` - *last release*: Jun 02, 2021, + *last release*: Dec 26, 2023, *status*: N/A, - *requires*: N/A + *requires*: pytest A timer plugin for pytest :pypi:`pytest-timestamper` - *last release*: Jun 06, 2021, + *last release*: Mar 27, 2024, *status*: N/A, *requires*: N/A Pytest plugin to add a timestamp prefix to the pytest output - :pypi:`pytest-tipsi-django` - *last release*: Nov 17, 2021, + :pypi:`pytest-timestamps` + *last release*: Sep 11, 2023, + *status*: N/A, + *requires*: pytest (>=7.3,<8.0) + + A simple plugin to view timestamps for each test + + :pypi:`pytest-tiny-api-client` + *last release*: Jan 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest + + The companion pytest plugin for tiny-api-client + + :pypi:`pytest-tinybird` + *last release*: Jun 26, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.0.0) + *requires*: pytest (>=3.8.0) + A pytest plugin to report test results to tinybird + :pypi:`pytest-tipsi-django` + *last release*: Feb 05, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=6.0.0 + + Better fixtures for django :pypi:`pytest-tipsi-testing` - *last release*: Nov 04, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=3.3.0) + *last release*: Feb 04, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=3.3.0 Better fixtures management. Various helpers :pypi:`pytest-tldr` - *last release*: Mar 12, 2021, + *last release*: Oct 26, 2022, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) @@ -7153,13 +10727,41 @@ This list contains 963 plugins. Cloud Jira Test Management (TM4J) PyTest reporter plugin + :pypi:`pytest-tmnet` + *last release*: Mar 01, 2022, + *status*: N/A, + *requires*: N/A + + A small example package + + :pypi:`pytest-tmp-files` + *last release*: Dec 08, 2023, + *status*: N/A, + *requires*: pytest + + Utilities to create temporary file hierarchies in pytest. + + :pypi:`pytest-tmpfs` + *last release*: Aug 29, 2022, + *status*: N/A, + *requires*: pytest + + A pytest plugin that helps you on using a temporary filesystem for testing. + :pypi:`pytest-tmreport` - *last release*: Nov 17, 2021, + *last release*: Aug 12, 2022, *status*: N/A, *requires*: N/A this is a vue-element ui report for pytest + :pypi:`pytest-tmux` + *last release*: Apr 22, 2023, + *status*: 4 - Beta, + *requires*: N/A + + A pytest plugin that enables tmux driven tests + :pypi:`pytest-todo` *last release*: May 23, 2019, *status*: 4 - Beta, @@ -7188,6 +10790,20 @@ This list contains 963 plugins. Numerous useful plugins for pytest. + :pypi:`pytest-toolkit` + *last release*: Apr 13, 2024, + *status*: N/A, + *requires*: N/A + + Useful utils for testing + + :pypi:`pytest-tools` + *last release*: Oct 21, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Pytest tools + :pypi:`pytest-tornado` *last release*: Jun 17, 2020, *status*: 5 - Production/Stable, @@ -7216,6 +10832,13 @@ This list contains 963 plugins. py.test plugin for testing Python 3.5+ Tornado code + :pypi:`pytest-trace` + *last release*: Jun 19, 2022, + *status*: N/A, + *requires*: pytest (>=4.6) + + Save OpenTelemetry spans generated during testing + :pypi:`pytest-track` *last release*: Feb 26, 2021, *status*: 3 - Alpha, @@ -7224,9 +10847,9 @@ This list contains 963 plugins. :pypi:`pytest-translations` - *last release*: Nov 05, 2021, + *last release*: Sep 11, 2023, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest (>=7) Test your translation files. @@ -7259,12 +10882,19 @@ This list contains 963 plugins. py.test plugin for using the same _trial_temp working directory as trial :pypi:`pytest-trio` - *last release*: Oct 16, 2020, + *last release*: Nov 01, 2022, *status*: N/A, - *requires*: N/A + *requires*: pytest (>=7.2.0) Pytest plugin for trio + :pypi:`pytest-trytond` + *last release*: Nov 04, 2022, + *status*: 4 - Beta, + *requires*: pytest (>=5) + + Pytest plugin for the Tryton server framework + :pypi:`pytest-tspwplib` *last release*: Jan 08, 2021, *status*: 4 - Beta, @@ -7272,6 +10902,13 @@ This list contains 963 plugins. A simple plugin to use with tspwplib + :pypi:`pytest-tst` + *last release*: Apr 27, 2022, + *status*: N/A, + *requires*: pytest (>=5.0.0) + + Customize pytest options, output and exit code to make it compatible with tst + :pypi:`pytest-tstcls` *last release*: Mar 23, 2020, *status*: 5 - Production/Stable, @@ -7279,15 +10916,57 @@ This list contains 963 plugins. Test Class Base + :pypi:`pytest-tui` + *last release*: Dec 08, 2023, + *status*: 4 - Beta, + *requires*: N/A + + Text User Interface (TUI) and HTML report for Pytest test runs + + :pypi:`pytest-tutorials` + *last release*: Mar 11, 2023, + *status*: N/A, + *requires*: N/A + + + + :pypi:`pytest-twilio-conversations-client-mock` + *last release*: Aug 02, 2022, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-twisted` - *last release*: Aug 30, 2021, + *last release*: Mar 19, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.3) + *requires*: pytest >=2.3 A twisted plugin for pytest. + :pypi:`pytest-typechecker` + *last release*: Feb 04, 2022, + *status*: N/A, + *requires*: pytest (>=6.2.5,<7.0.0) + + Run type checkers on specified test files + + :pypi:`pytest-typhoon-config` + *last release*: Apr 07, 2022, + *status*: 5 - Production/Stable, + *requires*: N/A + + A Typhoon HIL plugin that facilitates test parameter configuration at runtime + + :pypi:`pytest-typhoon-polarion` + *last release*: Feb 01, 2024, + *status*: 4 - Beta, + *requires*: N/A + + Typhoontest plugin for Siemens Polarion + :pypi:`pytest-typhoon-xray` - *last release*: Nov 03, 2021, + *last release*: Aug 15, 2023, *status*: 4 - Beta, *requires*: N/A @@ -7314,6 +10993,34 @@ This list contains 963 plugins. Text User Interface for running python tests + :pypi:`pytest-ui-failed-screenshot` + *last release*: Dec 06, 2022, + *status*: N/A, + *requires*: N/A + + UI自动测试失败时自动截图,并将截图加入到测试报告中 + + :pypi:`pytest-ui-failed-screenshot-allure` + *last release*: Dec 06, 2022, + *status*: N/A, + *requires*: N/A + + UI自动测试失败时自动截图,并将截图加入到Allure测试报告中 + + :pypi:`pytest-uncollect-if` + *last release*: Mar 24, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin to uncollect pytests tests rather than using skipif + + :pypi:`pytest-unflakable` + *last release*: Nov 12, 2023, + *status*: 4 - Beta, + *requires*: pytest >=6.2.0 + + Unflakable plugin for PyTest + :pypi:`pytest-unhandled-exception-exit-code` *last release*: Jun 22, 2020, *status*: 5 - Production/Stable, @@ -7321,6 +11028,13 @@ This list contains 963 plugins. Plugin for py.test set a different exit code on uncaught exceptions + :pypi:`pytest-unique` + *last release*: Sep 15, 2023, + *status*: N/A, + *requires*: pytest (>=7.4.2,<8.0.0) + + Pytest fixture to generate unique values. + :pypi:`pytest-unittest-filter` *last release*: Jan 12, 2019, *status*: 4 - Beta, @@ -7336,12 +11050,26 @@ This list contains 963 plugins. Run only unmarked tests :pypi:`pytest-unordered` - *last release*: Mar 28, 2021, + *last release*: Mar 13, 2024, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest >=7.0.0 Test equality of unordered collections in pytest + :pypi:`pytest-unstable` + *last release*: Sep 27, 2022, + *status*: 4 - Beta, + *requires*: N/A + + Set a test as unstable to return 0 even if it failed + + :pypi:`pytest-unused-fixtures` + *last release*: Apr 08, 2024, + *status*: 4 - Beta, + *requires*: pytest>7.3.2 + + A pytest plugin to list unused fixtures after a test run. + :pypi:`pytest-upload-report` *last release*: Jun 18, 2021, *status*: 5 - Production/Stable, @@ -7350,9 +11078,9 @@ This list contains 963 plugins. pytest-upload-report is a plugin for pytest that upload your test report for test results. :pypi:`pytest-utils` - *last release*: Dec 04, 2021, + *last release*: Feb 02, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.2.5,<7.0.0) + *requires*: pytest (>=7.0.0,<8.0.0) Some helpers for pytest. @@ -7371,14 +11099,14 @@ This list contains 963 plugins. :pypi:`pytest-variables` - *last release*: Oct 23, 2019, + *last release*: Feb 01, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=2.4.2) + *requires*: pytest>=7.0.0 pytest plugin for providing variables to tests/fixtures :pypi:`pytest-variant` - *last release*: Jun 20, 2021, + *last release*: Jun 06, 2022, *status*: N/A, *requires*: N/A @@ -7392,9 +11120,9 @@ This list contains 963 plugins. Plugin for managing VCR.py cassettes :pypi:`pytest-vcr-delete-on-fail` - *last release*: Aug 13, 2021, - *status*: 4 - Beta, - *requires*: pytest (>=6.2.2,<7.0.0) + *last release*: Feb 16, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest (>=8.0.0,<9.0.0) A pytest plugin that automates vcrpy cassettes deletion on test failure. @@ -7405,19 +11133,26 @@ This list contains 963 plugins. Test from HTTP interactions to dataframe processed. + :pypi:`pytest-vcs` + *last release*: Sep 22, 2022, + *status*: 4 - Beta, + *requires*: N/A + + + :pypi:`pytest-venv` - *last release*: Aug 04, 2020, + *last release*: Nov 23, 2023, *status*: 4 - Beta, *requires*: pytest py.test fixture for creating a virtual environment :pypi:`pytest-ver` - *last release*: Aug 30, 2021, - *status*: 2 - Pre-Alpha, - *requires*: N/A + *last release*: Feb 07, 2024, + *status*: 4 - Beta, + *requires*: pytest - Pytest module with Verification Report + Pytest module with Verification Protocol, Verification Report and Trace Matrix :pypi:`pytest-verbose-parametrize` *last release*: May 28, 2019, @@ -7440,6 +11175,20 @@ This list contains 963 plugins. Virtualenv fixture for py.test + :pypi:`pytest-visual` + *last release*: Nov 01, 2023, + *status*: 3 - Alpha, + *requires*: pytest >=7.0.0 + + + + :pypi:`pytest-vnc` + *last release*: Nov 06, 2023, + *status*: N/A, + *requires*: pytest + + VNC client for Pytest + :pypi:`pytest-voluptuous` *last release*: Jun 09, 2020, *status*: N/A, @@ -7454,6 +11203,13 @@ This list contains 963 plugins. A pytest plugin to easily enable debugging tests within Visual Studio Code + :pypi:`pytest-vscode-pycharm-cls` + *last release*: Feb 01, 2023, + *status*: N/A, + *requires*: pytest + + A PyTest helper to enable start remote debugger on test start or failure or when pytest.set_trace is used. + :pypi:`pytest-vts` *last release*: Jun 05, 2019, *status*: N/A, @@ -7461,6 +11217,13 @@ This list contains 963 plugins. pytest plugin for automatic recording of http stubbed tests + :pypi:`pytest-vulture` + *last release*: Jun 01, 2023, + *status*: N/A, + *requires*: pytest (>=7.0.0) + + A pytest plugin to checks dead code with vulture + :pypi:`pytest-vw` *last release*: Oct 07, 2015, *status*: 4 - Beta, @@ -7482,6 +11245,13 @@ This list contains 963 plugins. Pytest plugin for testing whatsapp bots with end to end tests + :pypi:`pytest-wake` + *last release*: Mar 20, 2024, + *status*: N/A, + *requires*: pytest + + + :pypi:`pytest-watch` *last release*: May 20, 2018, *status*: N/A, @@ -7490,11 +11260,18 @@ This list contains 963 plugins. Local continuous test runner with pytest and watchdog. :pypi:`pytest-watcher` - *last release*: Sep 18, 2021, - *status*: 3 - Alpha, + *last release*: Apr 01, 2024, + *status*: 4 - Beta, *requires*: N/A - Continiously runs pytest on changes in \*.py files + Automatically rerun your tests on file modifications + + :pypi:`pytest_wdb` + *last release*: Jul 04, 2016, + *status*: N/A, + *requires*: N/A + + Trace pytest tests with wdb to halt on error with --wdb. :pypi:`pytest-wdl` *last release*: Nov 17, 2020, @@ -7503,6 +11280,13 @@ This list contains 963 plugins. Pytest plugin for testing WDL workflows. + :pypi:`pytest-web3-data` + *last release*: Oct 04, 2023, + *status*: 4 - Beta, + *requires*: pytest + + A pytest plugin to fetch test data from IPFS HTTP gateways during pytest execution. + :pypi:`pytest-webdriver` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -7510,6 +11294,13 @@ This list contains 963 plugins. Selenium webdriver fixture for py.test + :pypi:`pytest-webtest-extras` + *last release*: Nov 13, 2023, + *status*: N/A, + *requires*: pytest >= 7.0.0 + + Pytest plugin to enhance pytest-html and allure reports of webtest projects by adding screenshots, comments and webpage sources. + :pypi:`pytest-wetest` *last release*: Nov 10, 2018, *status*: 4 - Beta, @@ -7517,6 +11308,13 @@ This list contains 963 plugins. Welian API Automation test framework pytest plugin + :pypi:`pytest-when` + *last release*: Mar 22, 2024, + *status*: N/A, + *requires*: pytest>=7.3.1 + + Utility which makes mocking more readable and controllable + :pypi:`pytest-whirlwind` *last release*: Jun 12, 2020, *status*: N/A, @@ -7545,6 +11343,13 @@ This list contains 963 plugins. Windows tray notifications for py.test results. + :pypi:`pytest-wiremock` + *last release*: Mar 27, 2022, + *status*: N/A, + *requires*: pytest (>=7.1.1,<8.0.0) + + A pytest plugin for programmatically using wiremock in integration tests + :pypi:`pytest-with-docker` *last release*: Nov 09, 2021, *status*: N/A, @@ -7553,18 +11358,18 @@ This list contains 963 plugins. pytest with docker helpers. :pypi:`pytest-workflow` - *last release*: Dec 03, 2021, + *last release*: Mar 18, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.4.0) + *requires*: pytest >=7.0.0 A pytest plugin for configuring workflow/pipeline tests using YAML files :pypi:`pytest-xdist` - *last release*: Sep 21, 2021, + *last release*: Apr 19, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.0.0) + *requires*: pytest >=6.2.0 - pytest xdist plugin for distributed testing and loop-on-failing modes + pytest xdist plugin for distributed testing, most importantly across multiple CPUs :pypi:`pytest-xdist-debug-for-graingert` *last release*: Jul 24, 2019, @@ -7587,6 +11392,13 @@ This list contains 963 plugins. pytest plugin helps to reproduce failures for particular xdist node + :pypi:`pytest-xdist-worker-stats` + *last release*: Apr 16, 2024, + *status*: 4 - Beta, + *requires*: pytest>=7.0.0 + + A pytest plugin to list worker statistics after a xdist run. + :pypi:`pytest-xfaillist` *last release*: Sep 17, 2021, *status*: N/A, @@ -7601,6 +11413,13 @@ This list contains 963 plugins. Pytest fixtures providing data read from function, module or package related (x)files. + :pypi:`pytest-xiuyu` + *last release*: Jul 25, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + This is a pytest plugin + :pypi:`pytest-xlog` *last release*: May 31, 2020, *status*: 4 - Beta, @@ -7608,6 +11427,13 @@ This list contains 963 plugins. Extended logging for test and decorators + :pypi:`pytest-xlsx` + *last release*: Mar 22, 2024, + *status*: N/A, + *requires*: N/A + + pytest plugin for generating test cases by xlsx(excel) + :pypi:`pytest-xpara` *last release*: Oct 30, 2017, *status*: 3 - Alpha, @@ -7616,9 +11442,9 @@ This list contains 963 plugins. An extended parametrizing plugin of pytest. :pypi:`pytest-xprocess` - *last release*: Jul 28, 2021, + *last release*: Mar 31, 2024, *status*: 4 - Beta, - *requires*: pytest (>=2.8) + *requires*: pytest>=2.8 A pytest plugin for managing processes across test runs. @@ -7637,18 +11463,32 @@ This list contains 963 plugins. :pypi:`pytest-xray-server` - *last release*: Oct 27, 2021, + *last release*: May 03, 2022, *status*: 3 - Alpha, *requires*: pytest (>=5.3.1) + :pypi:`pytest-xskynet` + *last release*: Feb 20, 2024, + *status*: N/A, + *requires*: N/A + + A package to prevent Dependency Confusion attacks against Yandex. + :pypi:`pytest-xvfb` - *last release*: Jun 09, 2020, + *last release*: May 29, 2023, *status*: 4 - Beta, *requires*: pytest (>=2.8.1) - A pytest plugin to run Xvfb for tests. + A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. + + :pypi:`pytest-xvirt` + *last release*: Oct 01, 2023, + *status*: 4 - Beta, + *requires*: pytest >=7.1.0 + + A pytest plugin to virtualize test. For example to transparently running them on a remote box. :pypi:`pytest-yaml` *last release*: Oct 05, 2018, @@ -7657,6 +11497,13 @@ This list contains 963 plugins. This plugin is used to load yaml output to your test using pytest framework. + :pypi:`pytest-yaml-sanmu` + *last release*: Apr 19, 2024, + *status*: N/A, + *requires*: pytest>=7.4.0 + + pytest plugin for generating test cases by yaml + :pypi:`pytest-yamltree` *last release*: Mar 02, 2020, *status*: 4 - Beta, @@ -7671,6 +11518,13 @@ This list contains 963 plugins. Run tests against wsgi apps defined in yaml + :pypi:`pytest-yaml-yoyo` + *last release*: Jun 19, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0) + + http/https API run by yaml + :pypi:`pytest-yapf` *last release*: Jul 06, 2017, *status*: 4 - Beta, @@ -7679,9 +11533,9 @@ This list contains 963 plugins. Run yapf :pypi:`pytest-yapf3` - *last release*: Aug 03, 2020, + *last release*: Mar 29, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.4) + *requires*: pytest (>=7) Validate your Python file format with yapf @@ -7692,10 +11546,17 @@ This list contains 963 plugins. PyTest plugin to run tests concurrently, each \`yield\` switch context to other one + :pypi:`pytest-yls` + *last release*: Mar 30, 2024, + *status*: N/A, + *requires*: pytest<8.0.0,>=7.2.2 + + Pytest plugin to test the YLS as a whole. + :pypi:`pytest-yuk` *last release*: Mar 26, 2021, *status*: N/A, - *requires*: N/A + *requires*: pytest>=5.0.0 Display tests you are uneasy with, using 🤢/🤮 for pass/fail of tests marked with yuk. @@ -7714,15 +11575,50 @@ This list contains 963 plugins. OWASP ZAP plugin for py.test. :pypi:`pytest-zebrunner` - *last release*: Dec 02, 2021, + *last release*: Jan 08, 2024, *status*: 5 - Production/Stable, *requires*: pytest (>=4.5.0) Pytest connector for Zebrunner reporting + :pypi:`pytest-zeebe` + *last release*: Feb 01, 2024, + *status*: N/A, + *requires*: pytest (>=7.4.2,<8.0.0) + + Pytest fixtures for testing Camunda 8 processes using a Zeebe test engine. + + :pypi:`pytest-zest` + *last release*: Nov 17, 2022, + *status*: N/A, + *requires*: N/A + + Zesty additions to pytest. + + :pypi:`pytest-zhongwen-wendang` + *last release*: Mar 04, 2024, + *status*: 4 - Beta, + *requires*: N/A + + PyTest 中文文档 + :pypi:`pytest-zigzag` *last release*: Feb 27, 2019, *status*: 4 - Beta, *requires*: pytest (~=3.6) Extend py.test for RPC OpenStack testing. + + :pypi:`pytest-zulip` + *last release*: May 07, 2022, + *status*: 5 - Production/Stable, + *requires*: pytest + + Pytest report plugin for Zulip + + :pypi:`pytest-zy` + *last release*: Mar 24, 2024, + *status*: N/A, + *requires*: pytest~=7.2.0 + + 接口自动化测试框架 diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/reference.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/reference.rst index 0d80c806807..4036b7d9912 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/reference.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/reference/reference.rst @@ -1,3 +1,5 @@ +:tocdepth: 3 + .. _`api-reference`: API Reference @@ -57,11 +59,19 @@ pytest.fail .. autofunction:: pytest.fail(reason, [pytrace=True, msg=None]) +.. class:: pytest.fail.Exception + + The exception raised by :func:`pytest.fail`. + pytest.skip ~~~~~~~~~~~ .. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None]) +.. class:: pytest.skip.Exception + + The exception raised by :func:`pytest.skip`. + .. _`pytest.importorskip ref`: pytest.importorskip @@ -74,14 +84,24 @@ pytest.xfail .. autofunction:: pytest.xfail +.. class:: pytest.xfail.Exception + + The exception raised by :func:`pytest.xfail`. + pytest.exit ~~~~~~~~~~~ -.. autofunction:: pytest.exit(reason, [returncode=False, msg=None]) +.. autofunction:: pytest.exit(reason, [returncode=None, msg=None]) + +.. class:: pytest.exit.Exception + + The exception raised by :func:`pytest.exit`. pytest.main ~~~~~~~~~~~ +**Tutorial**: :ref:`pytest.main-usage` + .. autofunction:: pytest.main pytest.param @@ -92,7 +112,7 @@ pytest.param pytest.raises ~~~~~~~~~~~~~ -**Tutorial**: :ref:`assertraises`. +**Tutorial**: :ref:`assertraises` .. autofunction:: pytest.raises(expected_exception: Exception [, *, match]) :with: excinfo @@ -100,15 +120,15 @@ pytest.raises pytest.deprecated_call ~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`ensuring_function_triggers`. +**Tutorial**: :ref:`ensuring_function_triggers` -.. autofunction:: pytest.deprecated_call() +.. autofunction:: pytest.deprecated_call([match]) :with: pytest.register_assert_rewrite ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`assertion-rewriting`. +**Tutorial**: :ref:`assertion-rewriting` .. autofunction:: pytest.register_assert_rewrite @@ -123,7 +143,7 @@ pytest.warns pytest.freeze_includes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`freezing-pytest`. +**Tutorial**: :ref:`freezing-pytest` .. autofunction:: pytest.freeze_includes @@ -132,7 +152,7 @@ pytest.freeze_includes Marks ----- -Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by +Marks can be used to apply metadata to *test functions* (but not fixtures), which can then be accessed by fixtures or plugins. @@ -143,7 +163,7 @@ fixtures or plugins. pytest.mark.filterwarnings ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`filterwarnings`. +**Tutorial**: :ref:`filterwarnings` Add warning filters to marked test items. @@ -160,8 +180,7 @@ Add warning filters to marked test items. .. code-block:: python @pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning") - def test_foo(): - ... + def test_foo(): ... .. _`pytest.mark.parametrize ref`: @@ -169,7 +188,7 @@ Add warning filters to marked test items. pytest.mark.parametrize ~~~~~~~~~~~~~~~~~~~~~~~ -:ref:`parametrize`. +**Tutorial**: :ref:`parametrize` This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see there. @@ -179,7 +198,7 @@ This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see pytest.mark.skip ~~~~~~~~~~~~~~~~ -:ref:`skip`. +**Tutorial**: :ref:`skip` Unconditionally skip a test function. @@ -193,7 +212,7 @@ Unconditionally skip a test function. pytest.mark.skipif ~~~~~~~~~~~~~~~~~~ -:ref:`skipif`. +**Tutorial**: :ref:`skipif` Skip a test function if a condition is ``True``. @@ -209,7 +228,7 @@ Skip a test function if a condition is ``True``. pytest.mark.usefixtures ~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`usefixtures`. +**Tutorial**: :ref:`usefixtures` Mark a test function as using the given fixture names. @@ -231,26 +250,28 @@ Mark a test function as using the given fixture names. pytest.mark.xfail ~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`xfail`. +**Tutorial**: :ref:`xfail` Marks a test function as *expected to fail*. -.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False) +.. py:function:: pytest.mark.xfail(condition=False, *, reason=None, raises=None, run=True, strict=xfail_strict) - :type condition: bool or str - :param condition: + :keyword Union[bool, str] condition: Condition for marking the test function as xfail (``True/False`` or a - :ref:`condition string <string conditions>`). If a bool, you also have + :ref:`condition string <string conditions>`). If a ``bool``, you also have to specify ``reason`` (see :ref:`condition string <string conditions>`). :keyword str reason: Reason why the test function is marked as xfail. - :keyword Type[Exception] raises: - Exception subclass expected to be raised by the test function; other exceptions will fail the test. + :keyword raises: + Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test. + Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works). + :type raises: Type[:py:exc:`Exception`] + :keyword bool run: - If the test function should actually be executed. If ``False``, the function will always xfail and will + Whether the test function should actually be executed. If ``False``, the function will always xfail and will not be executed (useful if a function is segfaulting). :keyword bool strict: - * If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails + * If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later. * If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it @@ -258,6 +279,8 @@ Marks a test function as *expected to fail*. that are always failing and there should be a clear indication if they unexpectedly start to pass (for example a new release of a library fixes a known bug). + Defaults to :confval:`xfail_strict`, which is ``False`` by default. + Custom marks ~~~~~~~~~~~~ @@ -269,8 +292,7 @@ For example: .. code-block:: python @pytest.mark.timeout(10, "slow", method="thread") - def test_function(): - ... + def test_function(): ... Will create and attach a :class:`Mark <pytest.Mark>` object to the collected :class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with @@ -287,17 +309,16 @@ Example for using multiple custom markers: @pytest.mark.timeout(10, "slow", method="thread") @pytest.mark.slow - def test_function(): - ... + def test_function(): ... -When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``. +When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``. .. _`fixtures-api`: Fixtures -------- -**Tutorial**: :ref:`fixture`. +**Tutorial**: :ref:`fixture` Fixtures are requested by test functions or other fixtures by declaring them as argument names. @@ -333,192 +354,96 @@ For more details, consult the full :ref:`fixtures docs <fixture>`. :decorator: -.. fixture:: cache - -config.cache -~~~~~~~~~~~~ - -**Tutorial**: :ref:`cache`. - -The ``config.cache`` object allows other plugins and fixtures -to store and retrieve values across test runs. To access it from fixtures -request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache``. - -Under the hood, the cache plugin uses the simple -``dumps``/``loads`` API of the :py:mod:`json` stdlib module. - -``config.cache`` is an instance of :class:`pytest.Cache`: - -.. autoclass:: pytest.Cache() - :members: - - -.. fixture:: capsys - -capsys -~~~~~~ - -:ref:`captures`. - -.. autofunction:: _pytest.capture.capsys() - :no-auto-options: - - Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. - - Example: - - .. code-block:: python - - def test_output(capsys): - print("hello") - captured = capsys.readouterr() - assert captured.out == "hello\n" - -.. autoclass:: pytest.CaptureFixture() - :members: - - -.. fixture:: capsysbinary - -capsysbinary -~~~~~~~~~~~~ - -:ref:`captures`. - -.. autofunction:: _pytest.capture.capsysbinary() - :no-auto-options: - - Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. - - Example: - - .. code-block:: python - - def test_output(capsysbinary): - print("hello") - captured = capsysbinary.readouterr() - assert captured.out == b"hello\n" - - .. fixture:: capfd capfd ~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capfd() :no-auto-options: - Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. - - Example: - - .. code-block:: python - - def test_system_echo(capfd): - os.system('echo "hello"') - captured = capfd.readouterr() - assert captured.out == "hello\n" - .. fixture:: capfdbinary capfdbinary ~~~~~~~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capfdbinary() :no-auto-options: - Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. - - Example: - - .. code-block:: python - - def test_system_echo(capfdbinary): - os.system('echo "hello"') - captured = capfdbinary.readouterr() - assert captured.out == b"hello\n" - -.. fixture:: doctest_namespace - -doctest_namespace -~~~~~~~~~~~~~~~~~ - -:ref:`doctest`. +.. fixture:: caplog -.. autofunction:: _pytest.doctest.doctest_namespace() +caplog +~~~~~~ - Usually this fixture is used in conjunction with another ``autouse`` fixture: +**Tutorial**: :ref:`logging` - .. code-block:: python +.. autofunction:: _pytest.logging.caplog() + :no-auto-options: - @pytest.fixture(autouse=True) - def add_np(doctest_namespace): - doctest_namespace["np"] = numpy + Returns a :class:`pytest.LogCaptureFixture` instance. - For more details: :ref:`doctest_namespace`. +.. autoclass:: pytest.LogCaptureFixture() + :members: -.. fixture:: request +.. fixture:: capsys -request -~~~~~~~ +capsys +~~~~~~ -:ref:`request example`. +**Tutorial**: :ref:`captures` -The ``request`` fixture is a special fixture providing information of the requesting test function. +.. autofunction:: _pytest.capture.capsys() + :no-auto-options: -.. autoclass:: pytest.FixtureRequest() +.. autoclass:: pytest.CaptureFixture() :members: +.. fixture:: capsysbinary -.. fixture:: pytestconfig - -pytestconfig +capsysbinary ~~~~~~~~~~~~ -.. autofunction:: _pytest.fixtures.pytestconfig() - - -.. fixture:: record_property +**Tutorial**: :ref:`captures` -record_property -~~~~~~~~~~~~~~~~~~~ +.. autofunction:: _pytest.capture.capsysbinary() + :no-auto-options: -**Tutorial**: :ref:`record_property example`. -.. autofunction:: _pytest.junitxml.record_property() +.. fixture:: cache +config.cache +~~~~~~~~~~~~ -.. fixture:: record_testsuite_property +**Tutorial**: :ref:`cache` -record_testsuite_property -~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``config.cache`` object allows other plugins and fixtures +to store and retrieve values across test runs. To access it from fixtures +request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache``. -**Tutorial**: :ref:`record_testsuite_property example`. +Under the hood, the cache plugin uses the simple +``dumps``/``loads`` API of the :py:mod:`json` stdlib module. -.. autofunction:: _pytest.junitxml.record_testsuite_property() +``config.cache`` is an instance of :class:`pytest.Cache`: +.. autoclass:: pytest.Cache() + :members: -.. fixture:: caplog -caplog -~~~~~~ - -:ref:`logging`. +.. fixture:: doctest_namespace -.. autofunction:: _pytest.logging.caplog() - :no-auto-options: +doctest_namespace +~~~~~~~~~~~~~~~~~ - Returns a :class:`pytest.LogCaptureFixture` instance. +**Tutorial**: :ref:`doctest` -.. autoclass:: pytest.LogCaptureFixture() - :members: +.. autofunction:: _pytest.doctest.doctest_namespace() .. fixture:: monkeypatch @@ -526,7 +451,7 @@ caplog monkeypatch ~~~~~~~~~~~ -:ref:`monkeypatching`. +**Tutorial**: :ref:`monkeypatching` .. autofunction:: _pytest.monkeypatch.monkeypatch() :no-auto-options: @@ -537,6 +462,14 @@ monkeypatch :members: +.. fixture:: pytestconfig + +pytestconfig +~~~~~~~~~~~~ + +.. autofunction:: _pytest.fixtures.pytestconfig() + + .. fixture:: pytester pytester @@ -573,18 +506,25 @@ To use it, include in your topmost ``conftest.py`` file: .. autoclass:: pytest.RecordedHookCall() :members: -.. fixture:: testdir -testdir -~~~~~~~ +.. fixture:: record_property -Identical to :fixture:`pytester`, but provides an instance whose methods return -legacy ``py.path.local`` objects instead when applicable. +record_property +~~~~~~~~~~~~~~~~~~~ -New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. +**Tutorial**: :ref:`record_property example` -.. autoclass:: pytest.Testdir() - :members: +.. autofunction:: _pytest.junitxml.record_property() + + +.. fixture:: record_testsuite_property + +record_testsuite_property +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Tutorial**: :ref:`record_testsuite_property example` + +.. autofunction:: _pytest.junitxml.record_testsuite_property() .. fixture:: recwarn @@ -600,11 +540,33 @@ recwarn .. autoclass:: pytest.WarningsRecorder() :members: -Each recorded warning is an instance of :class:`warnings.WarningMessage`. -.. note:: - ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated - differently; see :ref:`ensuring_function_triggers`. +.. fixture:: request + +request +~~~~~~~ + +**Example**: :ref:`request example` + +The ``request`` fixture is a special fixture providing information of the requesting test function. + +.. autoclass:: pytest.FixtureRequest() + :members: + + +.. fixture:: testdir + +testdir +~~~~~~~ + +Identical to :fixture:`pytester`, but provides an instance whose methods return +legacy ``py.path.local`` objects instead when applicable. + +New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + +.. autoclass:: pytest.Testdir() + :members: + :noindex: TimeoutExpired .. fixture:: tmp_path @@ -612,7 +574,7 @@ Each recorded warning is an instance of :class:`warnings.WarningMessage`. tmp_path ~~~~~~~~ -:ref:`tmp_path` +**Tutorial**: :ref:`tmp_path` .. autofunction:: _pytest.tmpdir.tmp_path() :no-auto-options: @@ -623,7 +585,7 @@ tmp_path tmp_path_factory ~~~~~~~~~~~~~~~~ -:ref:`tmp_path_factory example` +**Tutorial**: :ref:`tmp_path_factory example` .. _`tmp_path_factory factory api`: @@ -638,7 +600,7 @@ tmp_path_factory tmpdir ~~~~~~ -:ref:`tmpdir and tmpdir_factory` +**Tutorial**: :ref:`tmpdir and tmpdir_factory` .. autofunction:: _pytest.legacypath.LegacyTmpdirPlugin.tmpdir() :no-auto-options: @@ -649,7 +611,7 @@ tmpdir tmpdir_factory ~~~~~~~~~~~~~~ -:ref:`tmpdir and tmpdir_factory` +**Tutorial**: :ref:`tmpdir and tmpdir_factory` ``tmpdir_factory`` is an instance of :class:`~pytest.TempdirFactory`: @@ -662,12 +624,32 @@ tmpdir_factory Hooks ----- -:ref:`writing-plugins`. - -.. currentmodule:: _pytest.hookspec +**Tutorial**: :ref:`writing-plugins` Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`. +@pytest.hookimpl +~~~~~~~~~~~~~~~~ + +.. function:: pytest.hookimpl + :decorator: + + pytest's decorator for marking functions as hook implementations. + + See :ref:`writinghooks` and :func:`pluggy.HookimplMarker`. + +@pytest.hookspec +~~~~~~~~~~~~~~~~ + +.. function:: pytest.hookspec + :decorator: + + pytest's decorator for marking functions as hook specifications. + + See :ref:`declaringhooks` and :func:`pluggy.HookspecMarker`. + +.. currentmodule:: _pytest.hookspec + Bootstrapping hooks ~~~~~~~~~~~~~~~~~~~ @@ -675,8 +657,6 @@ Bootstrapping hooks called for plugins registered early enough (internal and set .. hook:: pytest_load_initial_conftests .. autofunction:: pytest_load_initial_conftests -.. hook:: pytest_cmdline_preparse -.. autofunction:: pytest_cmdline_preparse .. hook:: pytest_cmdline_parse .. autofunction:: pytest_cmdline_parse .. hook:: pytest_cmdline_main @@ -714,6 +694,8 @@ Collection hooks .. autofunction:: pytest_collection .. hook:: pytest_ignore_collect .. autofunction:: pytest_ignore_collect +.. hook:: pytest_collect_directory +.. autofunction:: pytest_collect_directory .. hook:: pytest_collect_file .. autofunction:: pytest_collect_file .. hook:: pytest_pycollect_makemodule @@ -808,8 +790,6 @@ Session related reporting hooks: .. autofunction:: pytest_fixture_setup .. hook:: pytest_fixture_post_finalizer .. autofunction:: pytest_fixture_post_finalizer -.. hook:: pytest_warning_captured -.. autofunction:: pytest_warning_captured .. hook:: pytest_warning_recorded .. autofunction:: pytest_warning_recorded @@ -844,23 +824,16 @@ reporting or interaction with exceptions: .. autofunction:: pytest_leave_pdb -Objects -------- - -Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`. - - -CallInfo -~~~~~~~~ - -.. autoclass:: pytest.CallInfo() - :members: +Collection tree objects +----------------------- +These are the collector and item classes (collectively called "nodes") which +make up the collection tree. -Class -~~~~~ +Node +~~~~ -.. autoclass:: pytest.Class() +.. autoclass:: _pytest.nodes.Node() :members: :show-inheritance: @@ -871,52 +844,52 @@ Collector :members: :show-inheritance: -CollectReport -~~~~~~~~~~~~~ +Item +~~~~ -.. autoclass:: pytest.CollectReport() +.. autoclass:: pytest.Item() :members: :show-inheritance: - :inherited-members: -Config -~~~~~~ +File +~~~~ -.. autoclass:: pytest.Config() +.. autoclass:: pytest.File() :members: + :show-inheritance: -ExceptionInfo -~~~~~~~~~~~~~ +FSCollector +~~~~~~~~~~~ -.. autoclass:: pytest.ExceptionInfo() +.. autoclass:: _pytest.nodes.FSCollector() :members: + :show-inheritance: +Session +~~~~~~~ -ExitCode -~~~~~~~~ - -.. autoclass:: pytest.ExitCode +.. autoclass:: pytest.Session() :members: + :show-inheritance: -File -~~~~ +Package +~~~~~~~ -.. autoclass:: pytest.File() +.. autoclass:: pytest.Package() :members: :show-inheritance: +Module +~~~~~~ -FixtureDef -~~~~~~~~~~ - -.. autoclass:: _pytest.fixtures.FixtureDef() +.. autoclass:: pytest.Module() :members: :show-inheritance: -FSCollector -~~~~~~~~~~~ +Class +~~~~~ -.. autoclass:: _pytest.nodes.FSCollector() +.. autoclass:: pytest.Class() :members: :show-inheritance: @@ -934,10 +907,64 @@ FunctionDefinition :members: :show-inheritance: -Item -~~~~ -.. autoclass:: pytest.Item() +Objects +------- + +Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>` +or importable from ``pytest``. + + +CallInfo +~~~~~~~~ + +.. autoclass:: pytest.CallInfo() + :members: + +CollectReport +~~~~~~~~~~~~~ + +.. autoclass:: pytest.CollectReport() + :members: + :show-inheritance: + :inherited-members: + +Config +~~~~~~ + +.. autoclass:: pytest.Config() + :members: + +Dir +~~~ + +.. autoclass:: pytest.Dir() + :members: + +Directory +~~~~~~~~~ + +.. autoclass:: pytest.Directory() + :members: + +ExceptionInfo +~~~~~~~~~~~~~ + +.. autoclass:: pytest.ExceptionInfo() + :members: + + +ExitCode +~~~~~~~~ + +.. autoclass:: pytest.ExitCode + :members: + + +FixtureDef +~~~~~~~~~~ + +.. autoclass:: pytest.FixtureDef() :members: :show-inheritance: @@ -968,19 +995,6 @@ Metafunc .. autoclass:: pytest.Metafunc() :members: -Module -~~~~~~ - -.. autoclass:: pytest.Module() - :members: - :show-inheritance: - -Node -~~~~ - -.. autoclass:: _pytest.nodes.Node() - :members: - Parser ~~~~~~ @@ -1002,13 +1016,6 @@ PytestPluginManager :inherited-members: :show-inheritance: -Session -~~~~~~~ - -.. autoclass:: pytest.Session() - :members: - :show-inheritance: - TestReport ~~~~~~~~~~ @@ -1017,10 +1024,16 @@ TestReport :show-inheritance: :inherited-members: -_Result +TestShortLogReport +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pytest.TestShortLogReport() + :members: + +Result ~~~~~~~ -Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information. +Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information. Stash ~~~~~ @@ -1108,11 +1121,24 @@ Environment Variables Environment variables that can be used to change pytest's behavior. +.. envvar:: CI + +When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable. + +.. envvar:: BUILD_NUMBER + +When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable. + .. envvar:: PYTEST_ADDOPTS This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given by the user, see :ref:`adding default options` for more information. +.. envvar:: PYTEST_VERSION + +This environment variable is defined at the start of the pytest session and is undefined afterwards. +It contains the value of ``pytest.__version__``, and among other things can be used to easily check if a code is running from within a pytest run. + .. envvar:: PYTEST_CURRENT_TEST This is not meant to be set by users, but is set by pytest internally with the name of the current test so other @@ -1151,19 +1177,22 @@ When set to ``0``, pytest will not use color. .. envvar:: NO_COLOR -When set (regardless of value), pytest will not use color in terminal output. +When set to a non-empty string (regardless of value), pytest will not use color in terminal output. ``PY_COLORS`` takes precedence over ``NO_COLOR``, which takes precedence over ``FORCE_COLOR``. See `no-color.org <https://no-color.org/>`__ for other libraries supporting this community standard. .. envvar:: FORCE_COLOR -When set (regardless of value), pytest will use color in terminal output. +When set to a non-empty string (regardless of value), pytest will use color in terminal output. ``PY_COLORS`` and ``NO_COLOR`` take precedence over ``FORCE_COLOR``. Exceptions ---------- -.. autoclass:: pytest.UsageError() +.. autoexception:: pytest.UsageError() + :show-inheritance: + +.. autoexception:: pytest.FixtureLookupError() :show-inheritance: .. _`warnings ref`: @@ -1194,6 +1223,12 @@ Custom warnings generated in some situations such as improper usage or deprecate .. autoclass:: pytest.PytestExperimentalApiWarning :show-inheritance: +.. autoclass:: pytest.PytestReturnNotNoneWarning + :show-inheritance: + +.. autoclass:: pytest.PytestRemovedIn9Warning + :show-inheritance: + .. autoclass:: pytest.PytestUnhandledCoroutineWarning :show-inheritance: @@ -1215,9 +1250,10 @@ Consult the :ref:`internal-warnings` section in the documentation for more infor Configuration Options --------------------- -Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``pyproject.toml``, ``tox.ini`` or ``setup.cfg`` -file, usually located at the root of your repository. To see each file format in details, see -:ref:`config file formats`. +Here is a list of builtin configuration options that may be written in a ``pytest.ini`` (or ``.pytest.ini``), +``pyproject.toml``, ``tox.ini``, or ``setup.cfg`` file, usually located at the root of your repository. + +To see each file format in details, see :ref:`config file formats`. .. warning:: Usage of ``setup.cfg`` is not recommended except for very simple use cases. ``.cfg`` @@ -1260,12 +1296,25 @@ passed multiple times. The expected format is ``name=value``. For example:: variables, that will be expanded. For more information about cache plugin please refer to :ref:`cache_provider`. +.. confval:: consider_namespace_packages + + Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ + when collecting Python modules. Default is ``False``. + + Set to ``True`` if the package you are testing is part of a namespace package. + + Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__ + are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__. + + .. versionadded:: 8.1 + .. confval:: console_output_style Sets the console output style while running tests: * ``classic``: classic pytest output. * ``progress``: like classic pytest output, but with a progress indicator. + * ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``. * ``count``: like progress, but shows progress as the number of tests completed instead of a percent. The default is ``progress``, but you can fallback to ``classic`` if you prefer or @@ -1516,7 +1565,7 @@ passed multiple times. The expected format is ``name=value``. For example:: - Sets a file name relative to the ``pytest.ini`` file where log messages should be written to, in addition + Sets a file name relative to the current working directory where log messages should be written to, in addition to the other logging facilities that are active. .. code-block:: ini @@ -1658,11 +1707,11 @@ passed multiple times. The expected format is ``name=value``. For example:: Additionally, ``pytest`` will attempt to intelligently identify and ignore a virtualenv by the presence of an activation script. Any directory deemed to be the root of a virtual environment will not be considered during test - collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that - ``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if + collection unless ``--collect-in-virtualenv`` is given. Note also that + ``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if you intend to run tests in a virtualenv with a base directory that matches ``'.*'`` you *must* override ``norecursedirs`` in addition to using the - ``‑‑collect‑in‑virtualenv`` flag. + ``--collect-in-virtualenv`` flag. .. confval:: python_classes @@ -1742,6 +1791,11 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] pythonpath = src1 src2 + .. note:: + + ``pythonpath`` does not affect some imports that happen very early, + most notably plugins loaded using the ``-p`` command line option. + .. confval:: required_plugins @@ -1758,11 +1812,12 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: testpaths - - Sets list of directories that should be searched for tests when no specific directories, files or test ids are given in the command line when executing pytest from the :ref:`rootdir <rootdir>` directory. + File system paths may use shell-style wildcards, including the recursive + ``**`` pattern. + Useful when all project tests are in a known location to speed up test collection and to avoid picking up undesired tests by accident. @@ -1771,8 +1826,51 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] testpaths = testing doc - This tells pytest to only look for tests in ``testing`` and ``doc`` - directories when executing from the root directory. + This configuration means that executing: + + .. code-block:: console + + pytest + + has the same practical effects as executing: + + .. code-block:: console + + pytest testing doc + + +.. confval:: tmp_path_retention_count + + + + How many sessions should we keep the `tmp_path` directories, + according to `tmp_path_retention_policy`. + + .. code-block:: ini + + [pytest] + tmp_path_retention_count = 3 + + Default: ``3`` + + +.. confval:: tmp_path_retention_policy + + + + Controls which directories created by the `tmp_path` fixture are kept around, + based on test outcome. + + * `all`: retains directories for all tests, regardless of the outcome. + * `failed`: retains directories only for tests with outcome `error` or `failed`. + * `none`: directories are always removed after each test ends, regardless of the outcome. + + .. code-block:: ini + + [pytest] + tmp_path_retention_policy = "all" + + Default: ``all`` .. confval:: usefixtures @@ -1788,6 +1886,32 @@ passed multiple times. The expected format is ``name=value``. For example:: clean_db +.. confval:: verbosity_assertions + + Set a verbosity level specifically for assertion related output, overriding the application wide level. + + .. code-block:: ini + + [pytest] + verbosity_assertions = 2 + + Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of + "auto" can be used to explicitly use the global verbosity level. + + +.. confval:: verbosity_test_cases + + Set a verbosity level specifically for test case execution related output, overriding the application wide level. + + .. code-block:: ini + + [pytest] + verbosity_test_cases = 2 + + Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of + "auto" can be used to explicitly use the global verbosity level. + + .. confval:: xfail_strict If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the @@ -1815,8 +1939,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: file_or_dir general: - -k EXPRESSION only run tests which match the given substring - expression. An expression is a python evaluatable + -k EXPRESSION Only run tests which match the given substring + expression. An expression is a Python evaluable expression where all names are substring-matched against test names and their parent classes. Example: -k 'test_method or test_other' matches all @@ -1830,93 +1954,98 @@ All the command-line flags can be obtained by running ``pytest --help``:: 'extra_keyword_matches' set, as well as functions which have names assigned directly to them. The matching is case-insensitive. - -m MARKEXPR only run tests matching given mark expression. - For example: -m 'mark1 and not mark2'. + -m MARKEXPR Only run tests matching given mark expression. For + example: -m 'mark1 and not mark2'. --markers show markers (builtin, plugin and per-project ones). - -x, --exitfirst exit instantly on first error or failed test. + -x, --exitfirst Exit instantly on first error or failed test --fixtures, --funcargs - show available fixtures, sorted by plugin appearance + Show available fixtures, sorted by plugin appearance (fixtures with leading '_' are only shown with '-v') - --fixtures-per-test show fixtures per test - --pdb start the interactive Python debugger on errors or - KeyboardInterrupt. + --fixtures-per-test Show fixtures per test + --pdb Start the interactive Python debugger on errors or + KeyboardInterrupt --pdbcls=modulename:classname - specify a custom interactive Python debugger for use + Specify a custom interactive Python debugger for use with --pdb.For example: --pdbcls=IPython.terminal.debugger:TerminalPdb - --trace Immediately break when running each test. - --capture=method per-test capturing method: one of fd|sys|no|tee-sys. - -s shortcut for --capture=no. - --runxfail report the results of xfail tests as if they were + --trace Immediately break when running each test + --capture=method Per-test capturing method: one of fd|sys|no|tee-sys + -s Shortcut for --capture=no + --runxfail Report the results of xfail tests as if they were not marked - --lf, --last-failed rerun only the tests that failed at the last run (or + --lf, --last-failed Rerun only the tests that failed at the last run (or all if none failed) - --ff, --failed-first run all tests, but run the last failures first. - This may re-order tests and thus lead to repeated - fixture setup/teardown. - --nf, --new-first run tests from new files first, then the rest of the + --ff, --failed-first Run all tests, but run the last failures first. This + may re-order tests and thus lead to repeated fixture + setup/teardown. + --nf, --new-first Run tests from new files first, then the rest of the tests sorted by file mtime --cache-show=[CACHESHOW] - show cache contents, don't perform collection or + Show cache contents, don't perform collection or tests. Optional argument: glob (default: '*'). - --cache-clear remove all cache contents at start of test run. + --cache-clear Remove all cache contents at start of test run --lfnf={all,none}, --last-failed-no-failures={all,none} - which tests to run with no previously (known) - failures. - --sw, --stepwise exit on test failure and continue from last failing + With ``--lf``, determines whether to execute tests + when there are no previously (known) failures or + when no cached ``lastfailed`` data was found. + ``all`` (the default) runs the full test suite + again. ``none`` just emits a message about no known + failures and exits successfully. + --sw, --stepwise Exit on test failure and continue from last failing test next time --sw-skip, --stepwise-skip - ignore the first failing test but stop on the next - failing test. - implicitly enables --stepwise. + Ignore the first failing test but stop on the next + failing test. Implicitly enables --stepwise. - reporting: - --durations=N show N slowest setup/test durations (N=0 for all). + Reporting: + --durations=N Show N slowest setup/test durations (N=0 for all) --durations-min=N Minimal duration in seconds for inclusion in slowest - list. Default 0.005 - -v, --verbose increase verbosity. - --no-header disable header - --no-summary disable summary - -q, --quiet decrease verbosity. - --verbosity=VERBOSE set verbosity. Default is 0. - -r chars show extra test summary info as specified by chars: + list. Default: 0.005. + -v, --verbose Increase verbosity + --no-header Disable header + --no-summary Disable summary + -q, --quiet Decrease verbosity + --verbosity=VERBOSE Set verbosity. Default: 0. + -r chars Show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), 'N' can be used to reset the list. (default: 'fE'). --disable-warnings, --disable-pytest-warnings - disable warnings summary - -l, --showlocals show locals in tracebacks (disabled by default). - --tb=style traceback print mode - (auto/long/short/line/native/no). + Disable warnings summary + -l, --showlocals Show locals in tracebacks (disabled by default) + --no-showlocals Hide locals in tracebacks (negate --showlocals + passed through addopts) + --tb=style Traceback print mode + (auto/long/short/line/native/no) --show-capture={no,stdout,stderr,log,all} Controls how captured stdout/stderr/log is shown on - failed tests. Default is 'all'. - --full-trace don't cut any tracebacks (default is to cut). - --color=color color terminal output (yes/no/auto). + failed tests. Default: all. + --full-trace Don't cut any tracebacks (default is to cut) + --color=color Color terminal output (yes/no/auto) --code-highlight={yes,no} Whether code should be highlighted (only if --color - is also enabled) - --pastebin=mode send failed|all info to bpaste.net pastebin service. - --junit-xml=path create junit-xml style report file at given path. - --junit-prefix=str prepend prefix to classnames in junit-xml output + is also enabled). Default: yes. + --pastebin=mode Send failed|all info to bpaste.net pastebin service + --junit-xml=path Create junit-xml style report file at given path + --junit-prefix=str Prepend prefix to classnames in junit-xml output pytest-warnings: -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS - set which warnings to report, see -W option of - python itself. - --maxfail=num exit after first num failures or errors. - --strict-config any warnings encountered while parsing the `pytest` - section of the configuration file raise errors. - --strict-markers markers not registered in the `markers` section of - the configuration file raise errors. - --strict (deprecated) alias to --strict-markers. - -c file load configuration from `file` instead of trying to + Set which warnings to report, see -W option of + Python itself + --maxfail=num Exit after first num failures or errors + --strict-config Any warnings encountered while parsing the `pytest` + section of the configuration file raise errors + --strict-markers Markers not registered in the `markers` section of + the configuration file raise errors + --strict (Deprecated) alias to --strict-markers + -c FILE, --config-file=FILE + Load configuration from `FILE` instead of trying to locate one of the implicit configuration files. --continue-on-collection-errors - Force test execution even if collection errors - occur. + Force test execution even if collection errors occur --rootdir=ROOTDIR Define root directory for tests. Can be relative path: 'root_dir', './root_dir', 'root_dir/another_dir/'; absolute path: @@ -1924,124 +2053,147 @@ All the command-line flags can be obtained by running ``pytest --help``:: '$HOME/root_dir'. collection: - --collect-only, --co only collect tests, don't execute them. - --pyargs try to interpret all arguments as python packages. - --ignore=path ignore path during collection (multi-allowed). - --ignore-glob=path ignore path pattern during collection (multi- - allowed). + --collect-only, --co Only collect tests, don't execute them + --pyargs Try to interpret all arguments as Python packages + --ignore=path Ignore path during collection (multi-allowed) + --ignore-glob=path Ignore path pattern during collection (multi- + allowed) --deselect=nodeid_prefix - deselect item (via node id prefix) during collection - (multi-allowed). - --confcutdir=dir only load conftest.py's relative to specified dir. - --noconftest Don't load any conftest.py files. - --keep-duplicates Keep duplicate tests. + Deselect item (via node id prefix) during collection + (multi-allowed) + --confcutdir=dir Only load conftest.py's relative to specified dir + --noconftest Don't load any conftest.py files + --keep-duplicates Keep duplicate tests --collect-in-virtualenv Don't ignore tests in a local virtualenv directory --import-mode={prepend,append,importlib} - prepend/append to sys.path when importing test - modules and conftest files, default is to prepend. - --doctest-modules run doctests in all .py modules + Prepend/append to sys.path when importing test + modules and conftest files. Default: prepend. + --doctest-modules Run doctests in all .py modules --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} - choose another output format for diffs on doctest + Choose another output format for diffs on doctest failure - --doctest-glob=pat doctests file matching pattern, default: test*.txt + --doctest-glob=pat Doctests file matching pattern, default: test*.txt --doctest-ignore-import-errors - ignore doctest ImportErrors + Ignore doctest collection errors --doctest-continue-on-failure - for a given doctest, continue to run after the first + For a given doctest, continue to run after the first failure test session debugging and configuration: - --basetemp=dir base temporary directory for this test run.(warning: - this directory is removed if it exists) - -V, --version display pytest version and information about + --basetemp=dir Base temporary directory for this test run. + (Warning: this directory is removed if it exists.) + -V, --version Display pytest version and information about plugins. When given twice, also display information about plugins. - -h, --help show help message and configuration info - -p name early-load given plugin module name or entry point - (multi-allowed). - To avoid loading of plugins, use the `no:` prefix, - e.g. `no:doctest`. - --trace-config trace considerations of conftest.py files. + -h, --help Show help message and configuration info + -p name Early-load given plugin module name or entry point + (multi-allowed). To avoid loading of plugins, use + the `no:` prefix, e.g. `no:doctest`. + --trace-config Trace considerations of conftest.py files --debug=[DEBUG_FILE_NAME] - store internal tracing debug information in this log - file. - This file is opened with 'w' and truncated as a - result, care advised. - Defaults to 'pytestdebug.log'. + Store internal tracing debug information in this log + file. This file is opened with 'w' and truncated as + a result, care advised. Default: pytestdebug.log. -o OVERRIDE_INI, --override-ini=OVERRIDE_INI - override ini option with "option=value" style, e.g. + Override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`. --assert=MODE Control assertion debugging tools. 'plain' performs no assertion debugging. 'rewrite' (the default) rewrites assert statements in test modules on import to provide assert expression information. - --setup-only only setup fixtures, do not execute tests. - --setup-show show setup of fixtures while executing tests. - --setup-plan show what fixtures and tests would be executed but - don't execute anything. + --setup-only Only setup fixtures, do not execute tests + --setup-show Show setup of fixtures while executing tests + --setup-plan Show what fixtures and tests would be executed but + don't execute anything logging: - --log-level=LEVEL level of messages to catch/display. - Not set by default, so it depends on the root/parent - log handler's effective level, where it is "WARNING" - by default. + --log-level=LEVEL Level of messages to catch/display. Not set by + default, so it depends on the root/parent log + handler's effective level, where it is "WARNING" by + default. --log-format=LOG_FORMAT - log format as used by the logging module. + Log format used by the logging module --log-date-format=LOG_DATE_FORMAT - log date format as used by the logging module. + Log date format used by the logging module --log-cli-level=LOG_CLI_LEVEL - cli logging level. + CLI logging level --log-cli-format=LOG_CLI_FORMAT - log format as used by the logging module. + Log format used by the logging module --log-cli-date-format=LOG_CLI_DATE_FORMAT - log date format as used by the logging module. - --log-file=LOG_FILE path to a file when logging will be written to. + Log date format used by the logging module + --log-file=LOG_FILE Path to a file when logging will be written to + --log-file-mode={w,a} + Log file open mode --log-file-level=LOG_FILE_LEVEL - log file logging level. + Log file logging level --log-file-format=LOG_FILE_FORMAT - log format as used by the logging module. + Log format used by the logging module --log-file-date-format=LOG_FILE_DATE_FORMAT - log date format as used by the logging module. + Log date format used by the logging module --log-auto-indent=LOG_AUTO_INDENT Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer. + --log-disable=LOGGER_DISABLE + Disable a logger by name. Can be passed multiple + times. - [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found: + [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found: - markers (linelist): markers for test functions + markers (linelist): Register new markers for test functions empty_parameter_set_mark (string): - default marker for empty parametersets - norecursedirs (args): directory patterns to avoid for recursion - testpaths (args): directories to search for tests when no files or - directories are given in the command line. + Default marker for empty parametersets + norecursedirs (args): Directory patterns to avoid for recursion + testpaths (args): Directories to search for tests when no files or + directories are given on the command line filterwarnings (linelist): Each line specifies a pattern for warnings.filterwarnings. Processed after -W/--pythonwarnings. - usefixtures (args): list of default fixtures to be used with this + consider_namespace_packages (bool): + Consider namespace packages when resolving module + names during import + usefixtures (args): List of default fixtures to be used with this project - python_files (args): glob-style file patterns for Python test module + python_files (args): Glob-style file patterns for Python test module discovery python_classes (args): - prefixes or glob names for Python test class + Prefixes or glob names for Python test class discovery python_functions (args): - prefixes or glob names for Python test function and + Prefixes or glob names for Python test function and method discovery disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool): - disable string escape non-ascii characters, might + Disable string escape non-ASCII characters, might cause unwanted side effects(use at your own risk) console_output_style (string): - console output: "classic", or with additional + Console output: "classic", or with additional progress information ("progress" (percentage) | - "count"). - xfail_strict (bool): default for the strict parameter of xfail markers + "count" | "progress-even-when-capture-no" (forces + progress even when capture=no) + verbosity_test_cases (string): + Specify a verbosity level for test case execution, + overriding the main level. Higher levels will + provide more detailed information about each test + case executed. + xfail_strict (bool): Default for the strict parameter of xfail markers when not given explicitly (default: False) + tmp_path_retention_count (string): + How many sessions should we keep the `tmp_path` + directories, according to + `tmp_path_retention_policy`. + tmp_path_retention_policy (string): + Controls which directories created by the `tmp_path` + fixture are kept around, based on test outcome. + (all/failed/none) enable_assertion_pass_hook (bool): - Enables the pytest_assertion_pass hook.Make sure to + Enables the pytest_assertion_pass hook. Make sure to delete any previously generated pyc cache files. + verbosity_assertions (string): + Specify a verbosity level for assertions, overriding + the main level. Higher levels will provide more + detailed explanation when an assertion fails. junit_suite_name (string): Test suite name for JUnit report junit_logging (string): @@ -2055,45 +2207,47 @@ All the command-line flags can be obtained by running ``pytest --help``:: junit_family (string): Emit XML for schema: one of legacy|xunit1|xunit2 doctest_optionflags (args): - option flags for doctests + Option flags for doctests doctest_encoding (string): - encoding used for doctest files - cache_dir (string): cache directory path. - log_level (string): default value for --log-level - log_format (string): default value for --log-format + Encoding used for doctest files + cache_dir (string): Cache directory path + log_level (string): Default value for --log-level + log_format (string): Default value for --log-format log_date_format (string): - default value for --log-date-format - log_cli (bool): enable log display during test run (also known as - "live logging"). + Default value for --log-date-format + log_cli (bool): Enable log display during test run (also known as + "live logging") log_cli_level (string): - default value for --log-cli-level + Default value for --log-cli-level log_cli_format (string): - default value for --log-cli-format + Default value for --log-cli-format log_cli_date_format (string): - default value for --log-cli-date-format - log_file (string): default value for --log-file + Default value for --log-cli-date-format + log_file (string): Default value for --log-file + log_file_mode (string): + Default value for --log-file-mode log_file_level (string): - default value for --log-file-level + Default value for --log-file-level log_file_format (string): - default value for --log-file-format + Default value for --log-file-format log_file_date_format (string): - default value for --log-file-date-format + Default value for --log-file-date-format log_auto_indent (string): - default value for --log-auto-indent + Default value for --log-auto-indent pythonpath (paths): Add paths to sys.path faulthandler_timeout (string): Dump the traceback of all threads if a test takes - more than TIMEOUT seconds to finish. - addopts (args): extra command line options - minversion (string): minimally required pytest version + more than TIMEOUT seconds to finish + addopts (args): Extra command line options + minversion (string): Minimally required pytest version required_plugins (args): - plugins that must be present for pytest to run + Plugins that must be present for pytest to run - environment variables: - PYTEST_ADDOPTS extra command line options - PYTEST_PLUGINS comma-separated plugins to load during startup - PYTEST_DISABLE_PLUGIN_AUTOLOAD set to disable plugin auto-loading - PYTEST_DEBUG set to enable debug tracing of pytest's internals + Environment variables: + PYTEST_ADDOPTS Extra command line options + PYTEST_PLUGINS Comma-separated plugins to load during startup + PYTEST_DISABLE_PLUGIN_AUTOLOAD Set to disable plugin auto-loading + PYTEST_DEBUG Set to enable debug tracing of pytest's internals to see available markers type: pytest --markers diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/requirements.txt b/tests/wpt/tests/tools/third_party/pytest/doc/en/requirements.txt index 5b49cb7fccc..974988c8cf4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/requirements.txt +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/requirements.txt @@ -1,7 +1,11 @@ pallets-sphinx-themes -pluggy>=1.0 -pygments-pytest>=2.2.0 +pluggy>=1.5.0 +pygments-pytest>=2.3.0 sphinx-removed-in>=0.2.0 -sphinx>=3.1,<4 +sphinx>=7 sphinxcontrib-trio sphinxcontrib-svg2pdfconverter +# Pin packaging because it no longer handles 'latest' version, which +# is the version that is assigned to the docs. +# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045. +packaging <22 diff --git a/tests/wpt/tests/tools/third_party/pytest/doc/en/talks.rst b/tests/wpt/tests/tools/third_party/pytest/doc/en/talks.rst index 6843c82bab5..b9b153a792e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/doc/en/talks.rst +++ b/tests/wpt/tests/tools/third_party/pytest/doc/en/talks.rst @@ -11,9 +11,16 @@ Books - `Python Testing with pytest, by Brian Okken (2017) <https://pragprog.com/book/bopytest/python-testing-with-pytest>`_. +- `Python Testing with pytest, Second Edition, by Brian Okken (2022) + <https://pragprog.com/titles/bopytest2/python-testing-with-pytest-second-edition>`_. + Talks and blog postings --------------------------------------------- +- Training: `pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=ofPHJrAOaTE>`_, Florian Bruhin, PyConDE 2022 + +- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021 + - Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020 - Webinar: `Simplify Your Tests with Fixtures <https://blog.jetbrains.com/pycharm/2020/08/webinar-recording-simplify-your-tests-with-fixtures-with-oliver-bestwalter/>`_, Oliver Bestwalter, via JetBrains, 2020 diff --git a/tests/wpt/tests/tools/third_party/pytest/extra/get_issues.py b/tests/wpt/tests/tools/third_party/pytest/extra/get_issues.py index 4aaa3c3ec31..716233ccba1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/extra/get_issues.py +++ b/tests/wpt/tests/tools/third_party/pytest/extra/get_issues.py @@ -3,6 +3,7 @@ from pathlib import Path import requests + issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues" diff --git a/tests/wpt/tests/tools/third_party/pytest/extra/setup-py.test/setup.py b/tests/wpt/tests/tools/third_party/pytest/extra/setup-py.test/setup.py deleted file mode 100644 index d0560ce1f5f..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/extra/setup-py.test/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -from distutils.core import setup - -if __name__ == "__main__": - if "sdist" not in sys.argv[1:]: - raise ValueError("please use 'pytest' pypi package instead of 'py.test'") - setup( - name="py.test", - version="0.0", - description="please use 'pytest' for installation", - ) diff --git a/tests/wpt/tests/tools/third_party/pytest/pyproject.toml b/tests/wpt/tests/tools/third_party/pytest/pyproject.toml index 5d32b755c74..01acfbf7660 100644 --- a/tests/wpt/tests/tools/third_party/pytest/pyproject.toml +++ b/tests/wpt/tests/tools/third_party/pytest/pyproject.toml @@ -1,15 +1,292 @@ +[project] +name = "pytest" +description = "pytest: simple powerful testing with Python" +readme = "README.rst" +keywords = [ + "test", + "unittest", +] +license = {text = "MIT"} +authors = [ + {name = "Holger Krekel"}, + {name = "Bruno Oliveira"}, + {name = "Ronny Pfannschmidt"}, + {name = "Floris Bruynooghe"}, + {name = "Brianna Laugher"}, + {name = "Florian Bruhin"}, + {name = "Others (See AUTHORS)"}, +] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Testing", + "Topic :: Utilities", +] +dynamic = [ + "version", +] +dependencies = [ + 'colorama; sys_platform == "win32"', + 'exceptiongroup>=1.0.0rc8; python_version < "3.11"', + "iniconfig", + "packaging", + "pluggy<2.0,>=1.5", + 'tomli>=1; python_version < "3.11"', +] +[project.optional-dependencies] +dev = [ + "argcomplete", + "attrs>=19.2", + "hypothesis>=3.56", + "mock", + "pygments>=2.7.2", + "requests", + "setuptools", + "xmlschema", +] +[project.urls] +Changelog = "https://docs.pytest.org/en/stable/changelog.html" +Homepage = "https://docs.pytest.org/en/latest/" +Source = "https://github.com/pytest-dev/pytest" +Tracker = "https://github.com/pytest-dev/pytest/issues" +Twitter = "https://twitter.com/pytestdotorg" +[project.scripts] +"py.test" = "pytest:console_main" +pytest = "pytest:console_main" + [build-system] +build-backend = "setuptools.build_meta" requires = [ - # sync with setup.py until we discard non-pep-517/518 - "setuptools>=45.0", - "setuptools-scm[toml]>=6.2.3", - "wheel", + "setuptools>=61", + "setuptools-scm[toml]>=6.2.3", ] -build-backend = "setuptools.build_meta" + +[tool.setuptools.package-data] +"_pytest" = ["py.typed"] +"pytest" = ["py.typed"] [tool.setuptools_scm] write_to = "src/_pytest/_version.py" +[tool.black] +target-version = ['py38'] + +[tool.ruff] +src = ["src"] +line-length = 88 + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + "B", # bugbear + "D", # pydocstyle + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "PYI", # flake8-pyi + "UP", # pyupgrade + "RUF", # ruff + "W", # pycodestyle + "PIE", # flake8-pie + "PGH004", # pygrep-hooks - Use specific rule codes when using noqa + "PLE", # pylint error + "PLW", # pylint warning + "PLR1714", # Consider merging multiple comparisons +] +ignore = [ + # bugbear ignore + "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. + "B007", # Loop control variable `i` not used within loop body + "B009", # Do not call `getattr` with a constant attribute value + "B010", # [*] Do not call `setattr` with a constant attribute value. + "B011", # Do not `assert False` (`python -O` removes these calls) + "B028", # No explicit `stacklevel` keyword argument found + # pycodestyle ignore + # pytest can do weird low-level things, and we usually know + # what we're doing when we use type(..) is ... + "E721", # Do not compare types, use `isinstance()` + # pydocstyle ignore + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D209", # [*] Multi-line docstring closing quotes should be on a separate line + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D402", # First line should not be the function's signature + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point + # ruff ignore + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + # pylint ignore + "PLW0603", # Using the global statement + "PLW0120", # remove the else and dedent its contents + "PLW2901", # for loop variable overwritten by assignment target + "PLR5501", # Use `elif` instead of `else` then `if` +] + +[tool.ruff.lint.pycodestyle] +# In order to be able to format for 88 char in ruff format +max-line-length = 120 + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.isort] +force-single-line = true +combine-as-imports = true +force-sort-within-sections = true +order-by-type = false +known-local-folder = ["pytest", "_pytest"] +lines-after-imports = 2 + +[tool.ruff.lint.per-file-ignores] +"src/_pytest/_py/**/*.py" = ["B", "PYI"] +"src/_pytest/_version.py" = ["I001"] +"testing/python/approx.py" = ["B015"] + +[tool.pylint.main] +# Maximum number of characters on a single line. +max-line-length = 120 +disable= [ + "abstract-method", + "arguments-differ", + "arguments-renamed", + "assigning-non-slot", + "attribute-defined-outside-init", + "bad-classmethod-argument", + "bad-mcs-method-argument", + "broad-exception-caught", + "broad-exception-raised", + "cell-var-from-loop", + "comparison-of-constants", + "comparison-with-callable", + "comparison-with-itself", + "condition-evals-to-constant", + "consider-using-dict-items", + "consider-using-enumerate", + "consider-using-from-import", + "consider-using-f-string", + "consider-using-in", + "consider-using-sys-exit", + "consider-using-ternary", + "consider-using-with", + "cyclic-import", + "disallowed-name", + "duplicate-code", + "eval-used", + "exec-used", + "expression-not-assigned", + "fixme", + "global-statement", + "implicit-str-concat", + "import-error", + "import-outside-toplevel", + "inconsistent-return-statements", + "invalid-bool-returned", + "invalid-name", + "invalid-repr-returned", + "invalid-str-returned", + "keyword-arg-before-vararg", + "line-too-long", + "method-hidden", + "misplaced-bare-raise", + "missing-docstring", + "missing-timeout", + "multiple-statements", + "no-else-break", + "no-else-continue", + "no-else-raise", + "no-else-return", + "no-member", + "no-name-in-module", + "no-self-argument", + "not-an-iterable", + "not-callable", + "pointless-exception-statement", + "pointless-statement", + "pointless-string-statement", + "protected-access", + "raise-missing-from", + "redefined-argument-from-local", + "redefined-builtin", + "redefined-outer-name", + "reimported", + "simplifiable-condition", + "simplifiable-if-expression", + "singleton-comparison", + "superfluous-parens", + "super-init-not-called", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-branches", + "too-many-function-args", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-nested-blocks", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "try-except-raise", + "typevar-name-incorrect-variance", + "unbalanced-tuple-unpacking", + "undefined-loop-variable", + "undefined-variable", + "unexpected-keyword-arg", + "unidiomatic-typecheck", + "unnecessary-comprehension", + "unnecessary-dunder-call", + "unnecessary-lambda", + "unnecessary-lambda-assignment", + "unpacking-non-sequence", + "unspecified-encoding", + "unsubscriptable-object", + "unused-argument", + "unused-import", + "unused-variable", + "used-before-assignment", + "use-dict-literal", + "use-implicit-booleaness-not-comparison", + "use-implicit-booleaness-not-len", + "useless-else-on-loop", + "useless-import-alias", + "useless-return", + "use-maxsplit-arg", + "using-constant-test", + "wrong-import-order", +] + +[tool.check-wheel-contents] +# check-wheel-contents is executed by the build-and-inspect-python-package action. +# W009: Wheel contains multiple toplevel library entries +ignore = "W009" + +[tool.pyproject-fmt] +indent = 4 + [tool.pytest.ini_options] minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" @@ -18,7 +295,12 @@ python_classes = ["Test", "Acceptance"] python_functions = ["test"] # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". testpaths = ["testing"] -norecursedirs = ["testing/example_scripts"] +norecursedirs = [ + "testing/example_scripts", + ".*", + "build", + "dist", +] xfail_strict = true filterwarnings = [ "error", @@ -28,8 +310,6 @@ filterwarnings = [ "default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*", # distutils is deprecated in 3.10, scheduled for removal in 3.12 "ignore:The distutils package is deprecated:DeprecationWarning", - # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)." - "ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))", # produced by pytest-xdist "ignore:.*type argument to addoption.*:DeprecationWarning", # produced on execnet (pytest-xdist) @@ -40,6 +320,9 @@ filterwarnings = [ # Those are caught/handled by pyupgrade, and not easy to filter with the # module being the filename (with .py removed). "default:invalid escape sequence:DeprecationWarning", + # ignore not yet fixed warnings for hook markers + "default:.*not marked using pytest.hook.*", + "ignore:.*not marked using pytest.hook.*::xdist.*", # ignore use of unregistered marks, because we use many to test the implementation "ignore::_pytest.warning_types.PytestUnknownMarkWarning", # https://github.com/benjaminp/six/issues/341 @@ -63,7 +346,6 @@ markers = [ "uses_pexpect", ] - [tool.towncrier] package = "pytest" package_dir = "src" @@ -112,5 +394,18 @@ template = "changelog/_template.rst" name = "Trivial/Internal Changes" showcontent = true -[tool.black] -target-version = ['py36'] +[tool.mypy] +files = ["src", "testing", "scripts"] +mypy_path = ["src"] +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_defs = true +ignore_missing_imports = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +no_implicit_reexport = true +warn_unused_ignores = true diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/.gitignore b/tests/wpt/tests/tools/third_party/pytest/scripts/.gitignore new file mode 100644 index 00000000000..50a75b62959 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/.gitignore @@ -0,0 +1 @@ +latest-release-notes.md diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/generate-gh-release-notes.py b/tests/wpt/tests/tools/third_party/pytest/scripts/generate-gh-release-notes.py new file mode 100644 index 00000000000..4222702d5d4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/generate-gh-release-notes.py @@ -0,0 +1,67 @@ +# mypy: disallow-untyped-defs +""" +Script used to generate a Markdown file containing only the changelog entries of a specific pytest release, which +is then published as a GitHub Release during deploy (see workflows/deploy.yml). + +The script requires ``pandoc`` to be previously installed in the system -- we need to convert from RST (the format of +our CHANGELOG) into Markdown (which is required by GitHub Releases). + +Requires Python3.6+. +""" + +from pathlib import Path +import re +import sys +from typing import Sequence + +import pypandoc + + +def extract_changelog_entries_for(version: str) -> str: + p = Path(__file__).parent.parent / "doc/en/changelog.rst" + changelog_lines = p.read_text(encoding="UTF-8").splitlines() + + title_regex = re.compile(r"pytest (\d\.\d+\.\d+\w*) \(\d{4}-\d{2}-\d{2}\)") + consuming_version = False + version_lines = [] + for line in changelog_lines: + m = title_regex.match(line) + if m: + # Found the version we want: start to consume lines until we find the next version title. + if m.group(1) == version: + consuming_version = True + # Found a new version title while parsing the version we want: break out. + elif consuming_version: + break + if consuming_version: + version_lines.append(line) + + return "\n".join(version_lines) + + +def convert_rst_to_md(text: str) -> str: + result = pypandoc.convert_text( + text, "md", format="rst", extra_args=["--wrap=preserve"] + ) + assert isinstance(result, str), repr(result) + return result + + +def main(argv: Sequence[str]) -> int: + if len(argv) != 3: + print("Usage: generate-gh-release-notes VERSION FILE") + return 2 + + version, filename = argv[1:3] + print(f"Generating GitHub release notes for version {version}") + rst_body = extract_changelog_entries_for(version) + md_body = convert_rst_to_md(rst_body) + Path(filename).write_text(md_body, encoding="UTF-8") + print() + print(f"Done: {filename}") + print() + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/prepare-release-pr.py b/tests/wpt/tests/tools/third_party/pytest/scripts/prepare-release-pr.py index 7a80de7edaa..7dabbd3b328 100644 --- a/tests/wpt/tests/tools/third_party/pytest/scripts/prepare-release-pr.py +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/prepare-release-pr.py @@ -1,3 +1,4 @@ +# mypy: disallow-untyped-defs """ This script is part of the pytest release process which is triggered manually in the Actions tab of the repository. @@ -12,9 +13,10 @@ After that, it will create a release using the `release` tox environment, and pu **Token**: currently the token from the GitHub Actions is used, pushed with `pytest bot <pytestbot@gmail.com>` commit author. """ + import argparse -import re from pathlib import Path +import re from subprocess import check_call from subprocess import check_output from subprocess import run @@ -31,10 +33,22 @@ class InvalidFeatureRelease(Exception): SLUG = "pytest-dev/pytest" PR_BODY = """\ -Created automatically from manual trigger. +Created by the [prepare release pr]\ +(https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow. + +Once all builds pass and it has been **approved** by one or more maintainers, start the \ +[deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters: + +* `Use workflow from`: `release-{version}`. +* `Release version`: `{version}`. + +Or execute on the command line: + +```console +gh workflow run deploy.yml -r release-{version} -f version={version} +``` -Once all builds pass and it has been **approved** by one or more maintainers, the build -can be released by pushing a tag `{version}` to this repository. +After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically. """ @@ -66,7 +80,7 @@ def prepare_release_pr( ) except InvalidFeatureRelease as e: print(f"{Fore.RED}{e}") - raise SystemExit(1) + raise SystemExit(1) from None print(f"Version: {Fore.CYAN}{version}") diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py b/tests/wpt/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py deleted file mode 100644 index 68cbd7adffd..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/scripts/publish-gh-release-notes.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Script used to publish GitHub release notes extracted from CHANGELOG.rst. - -This script is meant to be executed after a successful deployment in GitHub actions. - -Uses the following environment variables: - -* GIT_TAG: the name of the tag of the current commit. -* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions. - - Create one at: - - https://github.com/settings/tokens - - This token should be set in a secret in the repository, which is exposed as an - environment variable in the main.yml workflow file. - -The script also requires ``pandoc`` to be previously installed in the system. - -Requires Python3.6+. -""" -import os -import re -import sys -from pathlib import Path - -import github3 -import pypandoc - - -def publish_github_release(slug, token, tag_name, body): - github = github3.login(token=token) - owner, repo = slug.split("/") - repo = github.repository(owner, repo) - return repo.create_release(tag_name=tag_name, body=body) - - -def parse_changelog(tag_name): - p = Path(__file__).parent.parent / "doc/en/changelog.rst" - changelog_lines = p.read_text(encoding="UTF-8").splitlines() - - title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)") - consuming_version = False - version_lines = [] - for line in changelog_lines: - m = title_regex.match(line) - if m: - # found the version we want: start to consume lines until we find the next version title - if m.group(1) == tag_name: - consuming_version = True - # found a new version title while parsing the version we want: break out - elif consuming_version: - break - if consuming_version: - version_lines.append(line) - - return "\n".join(version_lines) - - -def convert_rst_to_md(text): - return pypandoc.convert_text( - text, "md", format="rst", extra_args=["--wrap=preserve"] - ) - - -def main(argv): - if len(argv) > 1: - tag_name = argv[1] - else: - tag_name = os.environ.get("GITHUB_REF") - if not tag_name: - print("tag_name not given and $GITHUB_REF not set", file=sys.stderr) - return 1 - if tag_name.startswith("refs/tags/"): - tag_name = tag_name[len("refs/tags/") :] - - token = os.environ.get("GH_RELEASE_NOTES_TOKEN") - if not token: - print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) - return 1 - - slug = os.environ.get("GITHUB_REPOSITORY") - if not slug: - print("GITHUB_REPOSITORY not set", file=sys.stderr) - return 1 - - rst_body = parse_changelog(tag_name) - md_body = convert_rst_to_md(rst_body) - if not publish_github_release(slug, token, tag_name, md_body): - print("Could not publish release notes:", file=sys.stderr) - print(md_body, file=sys.stderr) - return 5 - - print() - print(f"Release notes for {tag_name} published successfully:") - print(f"https://github.com/{slug}/releases/tag/{tag_name}") - print() - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/release.py b/tests/wpt/tests/tools/third_party/pytest/scripts/release.py index 19fef428428..bcbc4262d08 100644 --- a/tests/wpt/tests/tools/third_party/pytest/scripts/release.py +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/release.py @@ -1,4 +1,6 @@ +# mypy: disallow-untyped-defs """Invoke development tasks.""" + import argparse import os from pathlib import Path @@ -10,15 +12,15 @@ from colorama import Fore from colorama import init -def announce(version, template_name, doc_version): +def announce(version: str, template_name: str, doc_version: str) -> None: """Generates a new release announcement entry in the docs.""" # Get our list of authors - stdout = check_output(["git", "describe", "--abbrev=0", "--tags"]) - stdout = stdout.decode("utf-8") + stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8") last_version = stdout.strip() - stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"]) - stdout = stdout.decode("utf-8") + stdout = check_output( + ["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8" + ) contributors = { name @@ -61,7 +63,7 @@ def announce(version, template_name, doc_version): check_call(["git", "add", str(target)]) -def regen(version): +def regen(version: str) -> None: """Call regendoc tool to update examples and pytest output in the docs.""" print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs") check_call( @@ -70,7 +72,7 @@ def regen(version): ) -def fix_formatting(): +def fix_formatting() -> None: """Runs pre-commit in all files to ensure they are formatted correctly""" print( f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit" @@ -78,13 +80,15 @@ def fix_formatting(): call(["pre-commit", "run", "--all-files"]) -def check_links(): +def check_links() -> None: """Runs sphinx-build to check links""" print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links") check_call(["tox", "-e", "docs-checklinks"]) -def pre_release(version, template_name, doc_version, *, skip_check_links): +def pre_release( + version: str, template_name: str, doc_version: str, *, skip_check_links: bool +) -> None: """Generates new docs, release announcements and creates a local tag.""" announce(version, template_name, doc_version) regen(version) @@ -102,12 +106,12 @@ def pre_release(version, template_name, doc_version, *, skip_check_links): print("Please push your branch and open a PR.") -def changelog(version, write_out=False): +def changelog(version: str, write_out: bool = False) -> None: addopts = [] if write_out else ["--draft"] - check_call(["towncrier", "--yes", "--version", version] + addopts) + check_call(["towncrier", "--yes", "--version", version, *addopts]) -def main(): +def main() -> None: init(autoreset=True) parser = argparse.ArgumentParser() parser.add_argument("version", help="Release version") diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py b/tests/wpt/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py index 81507b40b75..f771295a01f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/towncrier-draft-to-file.py @@ -1,13 +1,16 @@ -import sys +# mypy: disallow-untyped-defs from subprocess import call +import sys -def main(): +def main() -> int: """ - Platform agnostic wrapper script for towncrier. - Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. + Platform-agnostic wrapper script for towncrier. + Fixes the issue (#7251) where Windows users are unable to natively run tox -e docs to build pytest docs. """ - with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: + with open( + "doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8" + ) as draft_file: return call(("towncrier", "--draft"), stdout=draft_file) diff --git a/tests/wpt/tests/tools/third_party/pytest/scripts/update-plugin-list.py b/tests/wpt/tests/tools/third_party/pytest/scripts/update-plugin-list.py index c034c72420b..6831fc984dd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/scripts/update-plugin-list.py +++ b/tests/wpt/tests/tools/third_party/pytest/scripts/update-plugin-list.py @@ -1,23 +1,49 @@ +# mypy: disallow-untyped-defs import datetime import pathlib import re from textwrap import dedent from textwrap import indent +from typing import Any +from typing import Iterable +from typing import Iterator +from typing import TypedDict import packaging.version -import requests +import platformdirs +from requests_cache import CachedResponse +from requests_cache import CachedSession +from requests_cache import OriginalResponse +from requests_cache import SQLiteCache import tabulate -import wcwidth from tqdm import tqdm +import wcwidth + FILE_HEAD = r""" +.. Note this file is autogenerated by scripts/update-plugin-list.py - usually weekly via github action + .. _plugin-list: -Plugin List -=========== +Pytest Plugin List +================== + +Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_. +It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects. +Packages classified as inactive are excluded. + +For detailed insights into how this list is generated, +please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_. + +.. warning:: + + Please be aware that this list is not a curated collection of projects + and does not undergo a systematic review process. + It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins. + + Do not presume any endorsement from the ``pytest`` project or its developers, + and always conduct your own quality assessment before incorporating any of these plugins into your own projects. -PyPI projects that match "pytest-\*" are considered plugins and are listed -automatically. Packages classified as inactive are excluded. .. The following conditional uses a different format for this list when creating a PDF, because otherwise the table gets far too wide for the @@ -33,6 +59,12 @@ DEVELOPMENT_STATUS_CLASSIFIERS = ( "Development Status :: 6 - Mature", "Development Status :: 7 - Inactive", ) +ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins + "logassert", + "logot", + "nuts", + "flask_fixture", +} def escape_rst(text: str) -> str: @@ -48,22 +80,62 @@ def escape_rst(text: str) -> str: return text -def iter_plugins(): - regex = r">([\d\w-]*)</a>" - response = requests.get("https://pypi.org/simple") +def project_response_with_refresh( + session: CachedSession, name: str, last_serial: int +) -> OriginalResponse | CachedResponse: + """Get a http cached pypi project + + force refresh in case of last serial mismatch + """ + response = session.get(f"https://pypi.org/pypi/{name}/json") + if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial: + response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True) + return response + + +def get_session() -> CachedSession: + """Configures the requests-cache session""" + cache_path = platformdirs.user_cache_path("pytest-plugin-list") + cache_path.mkdir(exist_ok=True, parents=True) + cache_file = cache_path.joinpath("http_cache.sqlite3") + return CachedSession(backend=SQLiteCache(cache_file)) - matches = list( - match - for match in re.finditer(regex, response.text) - if match.groups()[0].startswith("pytest-") + +def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]: + response = session.get( + "https://pypi.org/simple", + headers={"Accept": "application/vnd.pypi.simple.v1+json"}, + refresh=True, ) + return { + name: p["_last-serial"] + for p in response.json()["projects"] + if ( + (name := p["name"]).startswith(("pytest-", "pytest_")) + or name in ADDITIONAL_PROJECTS + ) + } + + +class PluginInfo(TypedDict): + """Relevant information about a plugin to generate the summary.""" + + name: str + summary: str + last_release: str + status: str + requires: str + - for match in tqdm(matches, smoothing=0): - name = match.groups()[0] - response = requests.get(f"https://pypi.org/pypi/{name}/json") +def iter_plugins() -> Iterator[PluginInfo]: + session = get_session() + name_2_serial = pytest_plugin_projects_from_pypi(session) + + for name, last_serial in tqdm(name_2_serial.items(), smoothing=0): + response = project_response_with_refresh(session, name, last_serial) if response.status_code == 404: - # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but - # return 404 on the JSON API. Skip. + # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple + # but return 404 on the JSON API. Skip. continue response.raise_for_status() info = response.json()["info"] @@ -78,11 +150,23 @@ def iter_plugins(): requires = "N/A" if info["requires_dist"]: for requirement in info["requires_dist"]: - if requirement == "pytest" or "pytest " in requirement: + if re.match(r"pytest(?![-.\w])", requirement): requires = requirement break + + def version_sort_key(version_string: str) -> Any: + """ + Return the sort key for the given version string + returned by the API. + """ + try: + return packaging.version.parse(version_string) + except packaging.version.InvalidVersion: + # Use a hard-coded pre-release version. + return packaging.version.Version("0.0.0alpha") + releases = response.json()["releases"] - for release in sorted(releases, key=packaging.version.parse, reverse=True): + for release in sorted(releases, key=version_sort_key, reverse=True): if releases[release]: release_date = datetime.date.fromisoformat( releases[release][-1]["upload_time_iso_8601"].split("T")[0] @@ -90,24 +174,25 @@ def iter_plugins(): last_release = release_date.strftime("%b %d, %Y") break name = f':pypi:`{info["name"]}`' - summary = escape_rst(info["summary"].replace("\n", "")) + summary = "" + if info["summary"]: + summary = escape_rst(info["summary"].replace("\n", "")) yield { "name": name, "summary": summary.strip(), - "last release": last_release, + "last_release": last_release, "status": status, "requires": requires, } -def plugin_definitions(plugins): +def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]: """Return RST for the plugin list that fits better on a vertical page.""" - for plugin in plugins: yield dedent( f""" {plugin['name']} - *last release*: {plugin["last release"]}, + *last release*: {plugin["last_release"]}, *status*: {plugin["status"]}, *requires*: {plugin["requires"]} @@ -116,18 +201,18 @@ def plugin_definitions(plugins): ) -def main(): - plugins = list(iter_plugins()) +def main() -> None: + plugins = [*iter_plugins()] reference_dir = pathlib.Path("doc", "en", "reference") plugin_list = reference_dir / "plugin_list.rst" - with plugin_list.open("w") as f: + with plugin_list.open("w", encoding="UTF-8") as f: f.write(FILE_HEAD) f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(".. only:: not latex\n\n") - wcwidth # reference library that must exist for tabulate to work + _ = wcwidth # reference library that must exist for tabulate to work plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst") f.write(indent(plugin_table, " ")) f.write("\n\n") diff --git a/tests/wpt/tests/tools/third_party/pytest/setup.cfg b/tests/wpt/tests/tools/third_party/pytest/setup.cfg deleted file mode 100644 index 26a5d2e63e5..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/setup.cfg +++ /dev/null @@ -1,105 +0,0 @@ -[metadata] -name = pytest -description = pytest: simple powerful testing with Python -long_description = file: README.rst -long_description_content_type = text/x-rst -url = https://docs.pytest.org/en/latest/ -author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others -license = MIT -license_file = LICENSE -platforms = unix, linux, osx, cygwin, win32 -classifiers = - Development Status :: 6 - Mature - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: MacOS :: MacOS X - Operating System :: Microsoft :: Windows - Operating System :: POSIX - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Topic :: Software Development :: Libraries - Topic :: Software Development :: Testing - Topic :: Utilities -keywords = test, unittest -project_urls = - Changelog=https://docs.pytest.org/en/stable/changelog.html - Twitter=https://twitter.com/pytestdotorg - Source=https://github.com/pytest-dev/pytest - Tracker=https://github.com/pytest-dev/pytest/issues - -[options] -packages = - _pytest - _pytest._code - _pytest._io - _pytest.assertion - _pytest.config - _pytest.mark - pytest -install_requires = - attrs>=19.2.0 - iniconfig - packaging - pluggy>=0.12,<2.0 - py>=1.8.2 - tomli>=1.0.0 - atomicwrites>=1.0;sys_platform=="win32" - colorama;sys_platform=="win32" - importlib-metadata>=0.12;python_version<"3.8" -python_requires = >=3.6 -package_dir = - =src -setup_requires = - setuptools - setuptools-scm>=6.0 -zip_safe = no - -[options.entry_points] -console_scripts = - pytest=pytest:console_main - py.test=pytest:console_main - -[options.extras_require] -testing = - argcomplete - hypothesis>=3.56 - mock - nose - pygments>=2.7.2 - requests - xmlschema - -[options.package_data] -_pytest = py.typed -pytest = py.typed - -[build_sphinx] -source_dir = doc/en/ -build_dir = doc/build -all_files = 1 - -[check-manifest] -ignore = - src/_pytest/_version.py - -[devpi:upload] -formats = sdist.tgz,bdist_wheel - -[mypy] -mypy_path = src -check_untyped_defs = True -disallow_any_generics = True -ignore_missing_imports = True -no_implicit_optional = True -show_error_codes = True -strict_equality = True -warn_redundant_casts = True -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True -no_implicit_reexport = True diff --git a/tests/wpt/tests/tools/third_party/pytest/setup.py b/tests/wpt/tests/tools/third_party/pytest/setup.py deleted file mode 100644 index 7f1a1763ca9..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - -if __name__ == "__main__": - setup() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/__init__.py index 8a406c5c751..b694a5f244a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/__init__.py @@ -1,9 +1,10 @@ __all__ = ["__version__", "version_tuple"] try: - from ._version import version as __version__, version_tuple + from ._version import version as __version__ + from ._version import version_tuple except ImportError: # pragma: no cover # broken installation, we don't even try # unknown only works because we do poor mans version compare __version__ = "unknown" - version_tuple = (0, 0, "unknown") # type:ignore[assignment] + version_tuple = (0, 0, "unknown") diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py index 41d9d9407c7..c24f925202a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_argcomplete.py @@ -61,10 +61,11 @@ If things do not work right away: which should throw a KeyError: 'COMPLINE' (which is properly set by the global argcomplete script). """ + import argparse +from glob import glob import os import sys -from glob import glob from typing import Any from typing import List from typing import Optional @@ -78,15 +79,15 @@ class FastFilesCompleter: def __call__(self, prefix: str, **kwargs: Any) -> List[str]: # Only called on non option completions. - if os.path.sep in prefix[1:]: - prefix_dir = len(os.path.dirname(prefix) + os.path.sep) + if os.sep in prefix[1:]: + prefix_dir = len(os.path.dirname(prefix) + os.sep) else: prefix_dir = 0 completion = [] globbed = [] if "*" not in prefix and "?" not in prefix: # We are on unix, otherwise no bash. - if not prefix or prefix[-1] == os.path.sep: + if not prefix or prefix[-1] == os.sep: globbed.extend(glob(prefix + ".*")) prefix += "*" globbed.extend(glob(prefix)) @@ -108,7 +109,6 @@ if os.environ.get("_ARGCOMPLETE"): def try_argcomplete(parser: argparse.ArgumentParser) -> None: argcomplete.autocomplete(parser, always_complete_options=False) - else: def try_argcomplete(parser: argparse.ArgumentParser) -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py index 511d0dde661..b0a418e9555 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/__init__.py @@ -1,4 +1,5 @@ """Python inspection/code generation API.""" + from .code import Code from .code import ExceptionInfo from .code import filter_traceback @@ -9,6 +10,7 @@ from .code import TracebackEntry from .source import getrawcode from .source import Source + __all__ = [ "Code", "ExceptionInfo", diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/code.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/code.py index 5b758a88480..ee6a5597c2c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/code.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/code.py @@ -1,13 +1,15 @@ +# mypy: allow-untyped-defs import ast +import dataclasses import inspect -import os -import re -import sys -import traceback from inspect import CO_VARARGS from inspect import CO_VARKEYWORDS from io import StringIO +import os from pathlib import Path +import re +import sys +import traceback from traceback import format_exception_only from types import CodeType from types import FrameType @@ -16,23 +18,24 @@ from typing import Any from typing import Callable from typing import ClassVar from typing import Dict +from typing import Final +from typing import final from typing import Generic from typing import Iterable from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import overload from typing import Pattern from typing import Sequence from typing import Set +from typing import SupportsIndex from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -from weakref import ref -import attr import pluggy import _pytest @@ -43,18 +46,16 @@ from _pytest._code.source import Source from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.deprecated import check_ispytest from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath -if TYPE_CHECKING: - from typing_extensions import Literal - from typing_extensions import SupportsIndex - from weakref import ReferenceType - _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] class Code: @@ -191,25 +192,25 @@ class Frame: class TracebackEntry: """A single entry in a Traceback.""" - __slots__ = ("_rawentry", "_excinfo", "_repr_style") + __slots__ = ("_rawentry", "_repr_style") def __init__( self, rawentry: TracebackType, - excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, + repr_style: Optional['Literal["short", "long"]'] = None, ) -> None: - self._rawentry = rawentry - self._excinfo = excinfo - self._repr_style: Optional['Literal["short", "long"]'] = None + self._rawentry: "Final" = rawentry + self._repr_style: "Final" = repr_style + + def with_repr_style( + self, repr_style: Optional['Literal["short", "long"]'] + ) -> "TracebackEntry": + return TracebackEntry(self._rawentry, repr_style) @property def lineno(self) -> int: return self._rawentry.tb_lineno - 1 - def set_repr_style(self, mode: "Literal['short', 'long']") -> None: - assert mode in ("short", "long") - self._repr_style = mode - @property def frame(self) -> Frame: return Frame(self._rawentry.tb_frame) @@ -269,7 +270,7 @@ class TracebackEntry: source = property(getsource) - def ishidden(self) -> bool: + def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool: """Return True if the current frame has a var __tracebackhide__ resolving to True. @@ -278,9 +279,9 @@ class TracebackEntry: Mostly for internal use. """ - tbh: Union[ - bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool] - ] = False + tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( + False + ) for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): # in normal cases, f_locals and f_globals are dictionaries # however via `exec(...)` / `eval(...)` they can be other types @@ -293,7 +294,7 @@ class TracebackEntry: else: break if tbh and callable(tbh): - return tbh(None if self._excinfo is None else self._excinfo()) + return tbh(excinfo) return tbh def __str__(self) -> str: @@ -326,16 +327,14 @@ class Traceback(List[TracebackEntry]): def __init__( self, tb: Union[TracebackType, Iterable[TracebackEntry]], - excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: """Initialize from given python traceback object and ExceptionInfo.""" - self._excinfo = excinfo if isinstance(tb, TracebackType): def f(cur: TracebackType) -> Iterable[TracebackEntry]: cur_: Optional[TracebackType] = cur while cur_ is not None: - yield TracebackEntry(cur_, excinfo=excinfo) + yield TracebackEntry(cur_) cur_ = cur_.tb_next super().__init__(f(tb)) @@ -375,16 +374,14 @@ class Traceback(List[TracebackEntry]): continue if firstlineno is not None and x.frame.code.firstlineno != firstlineno: continue - return Traceback(x._rawentry, self._excinfo) + return Traceback(x._rawentry) return self @overload - def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: - ... + def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ... @overload - def __getitem__(self, key: slice) -> "Traceback": - ... + def __getitem__(self, key: slice) -> "Traceback": ... def __getitem__( self, key: Union["SupportsIndex", slice] @@ -395,26 +392,27 @@ class Traceback(List[TracebackEntry]): return super().__getitem__(key) def filter( - self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() + self, + excinfo_or_fn: Union[ + "ExceptionInfo[BaseException]", + Callable[[TracebackEntry], bool], + ], + /, ) -> "Traceback": - """Return a Traceback instance with certain items removed + """Return a Traceback instance with certain items removed. - fn is a function that gets a single argument, a TracebackEntry - instance, and should return True when the item should be added - to the Traceback, False when not. + If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s + which are hidden (see ishidden() above). - By default this removes all the TracebackEntries which are hidden - (see ishidden() above). + Otherwise, the filter is a function that gets a single argument, a + ``TracebackEntry`` instance, and should return True when the item should + be added to the ``Traceback``, False when not. """ - return Traceback(filter(fn, self), self._excinfo) - - def getcrashentry(self) -> TracebackEntry: - """Return last non-hidden traceback entry that lead to the exception of a traceback.""" - for i in range(-1, -len(self) - 1, -1): - entry = self[i] - if not entry.ishidden(): - return entry - return self[-1] + if isinstance(excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731 + else: + fn = excinfo_or_fn + return Traceback(filter(fn, self)) def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if @@ -426,15 +424,14 @@ class Traceback(List[TracebackEntry]): # which generates code objects that have hash/value equality # XXX needs a test key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno - # print "checking for recursion at", key values = cache.setdefault(key, []) + # Since Python 3.13 f_locals is a proxy, freeze it. + loc = dict(entry.frame.f_locals) if values: - f = entry.frame - loc = f.f_locals for otherloc in values: if otherloc == loc: return i - values.append(entry.frame.f_locals) + values.append(loc) return None @@ -442,7 +439,7 @@ E = TypeVar("E", bound=BaseException, covariant=True) @final -@attr.s(repr=False, init=False, auto_attribs=True) +@dataclasses.dataclass class ExceptionInfo(Generic[E]): """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" @@ -466,22 +463,42 @@ class ExceptionInfo(Generic[E]): self._traceback = traceback @classmethod - def from_exc_info( + def from_exception( cls, - exc_info: Tuple[Type[E], E, TracebackType], + # Ignoring error: "Cannot use a covariant type variable as a parameter". + # This is OK to ignore because this class is (conceptually) readonly. + # See https://github.com/python/mypy/issues/7049. + exception: E, # type: ignore[misc] exprinfo: Optional[str] = None, ) -> "ExceptionInfo[E]": - """Return an ExceptionInfo for an existing exc_info tuple. + """Return an ExceptionInfo for an existing exception. - .. warning:: - - Experimental API + The exception must have a non-``None`` ``__traceback__`` attribute, + otherwise this function fails with an assertion error. This means that + the exception must have been raised, or added a traceback with the + :py:meth:`~BaseException.with_traceback()` method. :param exprinfo: A text string helping to determine if we should strip ``AssertionError`` from the output. Defaults to the exception message/``__str__()``. + + .. versionadded:: 7.4 """ + assert exception.__traceback__, ( + "Exceptions passed to ExcInfo.from_exception(...)" + " must have a non-None __traceback__." + ) + exc_info = (type(exception), exception, exception.__traceback__) + return cls.from_exc_info(exc_info, exprinfo) + + @classmethod + def from_exc_info( + cls, + exc_info: Tuple[Type[E], E, TracebackType], + exprinfo: Optional[str] = None, + ) -> "ExceptionInfo[E]": + """Like :func:`from_exception`, but using old-style exc_info tuple.""" _striptext = "" if exprinfo is None and isinstance(exc_info[1], AssertionError): exprinfo = getattr(exc_info[1], "msg", None) @@ -560,7 +577,7 @@ class ExceptionInfo(Generic[E]): def traceback(self) -> Traceback: """The traceback.""" if self._traceback is None: - self._traceback = Traceback(self.tb, excinfo=ref(self)) + self._traceback = Traceback(self.tb) return self._traceback @traceback.setter @@ -570,9 +587,7 @@ class ExceptionInfo(Generic[E]): def __repr__(self) -> str: if self._excinfo is None: return "<ExceptionInfo for raises contextmanager>" - return "<{} {} tblen={}>".format( - self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback) - ) + return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>" def exconly(self, tryshort: bool = False) -> str: """Return the exception as a string. @@ -599,18 +614,25 @@ class ExceptionInfo(Generic[E]): """ return isinstance(self.value, exc) - def _getreprcrash(self) -> "ReprFileLocation": - exconly = self.exconly(tryshort=True) - entry = self.traceback.getcrashentry() - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno + 1, exconly) + def _getreprcrash(self) -> Optional["ReprFileLocation"]: + # Find last non-hidden traceback entry that led to the exception of the + # traceback, or None if all hidden. + for i in range(-1, -len(self.traceback) - 1, -1): + entry = self.traceback[i] + if not entry.ishidden(self): + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + exconly = self.exconly(tryshort=True) + return ReprFileLocation(path, lineno + 1, exconly) + return None def getrepr( self, showlocals: bool = False, - style: "_TracebackStyle" = "long", + style: _TracebackStyle = "long", abspath: bool = False, - tbfilter: bool = True, + tbfilter: Union[ + bool, Callable[["ExceptionInfo[BaseException]"], Traceback] + ] = True, funcargs: bool = False, truncate_locals: bool = True, chain: bool = True, @@ -622,14 +644,20 @@ class ExceptionInfo(Generic[E]): Ignored if ``style=="native"``. :param str style: - long|short|no|native|value traceback style. + long|short|line|no|native|value traceback style. :param bool abspath: If paths should be changed to absolute or left unchanged. - :param bool tbfilter: - Hide entries that contain a local variable ``__tracebackhide__==True``. - Ignored if ``style=="native"``. + :param tbfilter: + A filter for traceback entries. + + * If false, don't hide any entries. + * If true, hide internal entries and entries that contain a local + variable ``__tracebackhide__ = True``. + * If a callable, delegates the filtering to the callable. + + Ignored if ``style`` is ``"native"``. :param bool funcargs: Show fixtures ("funcargs" for legacy purposes) per traceback entry. @@ -646,12 +674,14 @@ class ExceptionInfo(Generic[E]): """ if style == "native": return ReprExceptionInfo( - ReprTracebackNative( + reprtraceback=ReprTracebackNative( traceback.format_exception( - self.type, self.value, self.traceback[0]._rawentry + self.type, + self.value, + self.traceback[0]._rawentry if self.traceback else None, ) ), - self._getreprcrash(), + reprcrash=self._getreprcrash(), ) fmt = FormattedExcinfo( @@ -665,6 +695,25 @@ class ExceptionInfo(Generic[E]): ) return fmt.repr_excinfo(self) + def _stringify_exception(self, exc: BaseException) -> str: + try: + notes = getattr(exc, "__notes__", []) + except KeyError: + # Workaround for https://github.com/python/cpython/issues/98778 on + # Python <= 3.9, and some 3.10 and 3.11 patch versions. + HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) + if sys.version_info < (3, 12) and isinstance(exc, HTTPError): + notes = [] + else: + raise + + return "\n".join( + [ + str(exc), + *notes, + ] + ) + def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. @@ -672,15 +721,81 @@ class ExceptionInfo(Generic[E]): If it matches `True` is returned, otherwise an `AssertionError` is raised. """ __tracebackhide__ = True - msg = "Regex pattern {!r} does not match {!r}." - if regexp == str(self.value): - msg += " Did you mean to `re.escape()` the regex?" - assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value)) + value = self._stringify_exception(self.value) + msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" + if regexp == value: + msg += "\n Did you mean to `re.escape()` the regex?" + assert re.search(regexp, value), msg # Return True to allow for "assert excinfo.match()". return True + def _group_contains( + self, + exc_group: BaseExceptionGroup[BaseException], + expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], + match: Union[str, Pattern[str], None], + target_depth: Optional[int] = None, + current_depth: int = 1, + ) -> bool: + """Return `True` if a `BaseExceptionGroup` contains a matching exception.""" + if (target_depth is not None) and (current_depth > target_depth): + # already descended past the target depth + return False + for exc in exc_group.exceptions: + if isinstance(exc, BaseExceptionGroup): + if self._group_contains( + exc, expected_exception, match, target_depth, current_depth + 1 + ): + return True + if (target_depth is not None) and (current_depth != target_depth): + # not at the target depth, no match + continue + if not isinstance(exc, expected_exception): + continue + if match is not None: + value = self._stringify_exception(exc) + if not re.search(match, value): + continue + return True + return False + + def group_contains( + self, + expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], + *, + match: Union[str, Pattern[str], None] = None, + depth: Optional[int] = None, + ) -> bool: + """Check whether a captured exception group contains a matching exception. + + :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. + + :param str | Pattern[str] | None match: + If specified, a string containing a regular expression, + or a regular expression object, that is tested against the string + representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__` + using :func:`re.search`. -@attr.s(auto_attribs=True) + To match a literal string that may contain :ref:`special characters + <re-syntax>`, the pattern can first be escaped with :func:`re.escape`. + + :param Optional[int] depth: + If `None`, will search for a matching exception at any nesting depth. + If >= 1, will only match an exception if it's at the specified depth (depth = 1 being + the exceptions contained within the topmost exception group). + + .. versionadded:: 8.0 + """ + msg = "Captured exception is not an instance of `BaseExceptionGroup`" + assert isinstance(self.value, BaseExceptionGroup), msg + msg = "`depth` must be >= 1 if specified" + assert (depth is None) or (depth >= 1), msg + return self._group_contains(self.value, expected_exception, match, depth) + + +@dataclasses.dataclass class FormattedExcinfo: """Presenting information about failing Functions and Generators.""" @@ -689,14 +804,14 @@ class FormattedExcinfo: fail_marker: ClassVar = "E" showlocals: bool = False - style: "_TracebackStyle" = "long" + style: _TracebackStyle = "long" abspath: bool = True - tbfilter: bool = True + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False truncate_locals: bool = True chain: bool = True - astcache: Dict[Union[str, Path], ast.AST] = attr.ib( - factory=dict, init=False, repr=False + astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field( + default_factory=dict, init=False, repr=False ) def _getindent(self, source: "Source") -> int: @@ -737,11 +852,13 @@ class FormattedExcinfo: ) -> List[str]: """Return formatted and marked up source lines.""" lines = [] - if source is None or line_index >= len(source.lines): + if source is not None and line_index < 0: + line_index += len(source) + if source is None or line_index >= len(source.lines) or line_index < 0: + # `line_index` could still be outside `range(len(source.lines))` if + # we're processing AST with pathological position attributes. source = Source("???") line_index = 0 - if line_index < 0: - line_index += len(source) space_prefix = " " if short: lines.append(space_prefix + source.lines[line_index].strip()) @@ -801,12 +918,16 @@ class FormattedExcinfo: def repr_traceback_entry( self, - entry: TracebackEntry, + entry: Optional[TracebackEntry], excinfo: Optional[ExceptionInfo[BaseException]] = None, ) -> "ReprEntry": lines: List[str] = [] - style = entry._repr_style if entry._repr_style is not None else self.style - if style in ("short", "long"): + style = ( + entry._repr_style + if entry is not None and entry._repr_style is not None + else self.style + ) + if style in ("short", "long") and entry is not None: source = self._getentrysource(entry) if source is None: source = Source("???") @@ -847,25 +968,31 @@ class FormattedExcinfo: def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback - if self.tbfilter: - traceback = traceback.filter() + if callable(self.tbfilter): + traceback = self.tbfilter(excinfo) + elif self.tbfilter: + traceback = traceback.filter(excinfo) if isinstance(excinfo.value, RecursionError): traceback, extraline = self._truncate_recursive_traceback(traceback) else: extraline = None + if not traceback: + if extraline is None: + extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." + entries = [self.repr_traceback_entry(None, excinfo)] + return ReprTraceback(entries, extraline, style=self.style) + last = traceback[-1] - entries = [] if self.style == "value": - reprentry = self.repr_traceback_entry(last, excinfo) - entries.append(reprentry) + entries = [self.repr_traceback_entry(last, excinfo)] return ReprTraceback(entries, None, style=self.style) - for index, entry in enumerate(traceback): - einfo = (last == entry) and excinfo or None - reprentry = self.repr_traceback_entry(entry, einfo) - entries.append(reprentry) + entries = [ + self.repr_traceback_entry(entry, excinfo if last == entry else None) + for entry in traceback + ] return ReprTraceback(entries, extraline, style=self.style) def _truncate_recursive_traceback( @@ -890,13 +1017,8 @@ class FormattedExcinfo: extraline: Optional[str] = ( "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" " The following exception happened when comparing locals in the stack frame:\n" - " {exc_type}: {exc_msg}\n" - " Displaying first and last {max_frames} stack frames out of {total}." - ).format( - exc_type=type(e).__name__, - exc_msg=str(e), - max_frames=max_frames, - total=len(traceback), + f" {type(e).__name__}: {e!s}\n" + f" Displaying first and last {max_frames} stack frames out of {len(traceback)}." ) # Type ignored because adding two instances of a List subtype # currently incorrectly has type List instead of the subtype. @@ -922,11 +1044,24 @@ class FormattedExcinfo: seen: Set[int] = set() while e is not None and id(e) not in seen: seen.add(id(e)) + if excinfo_: - reprtraceback = self.repr_traceback(excinfo_) - reprcrash: Optional[ReprFileLocation] = ( - excinfo_._getreprcrash() if self.style != "value" else None - ) + # Fall back to native traceback as a temporary workaround until + # full support for exception groups added to ExceptionInfo. + # See https://github.com/pytest-dev/pytest/issues/9159 + if isinstance(e, BaseExceptionGroup): + reprtraceback: Union[ReprTracebackNative, ReprTraceback] = ( + ReprTracebackNative( + traceback.format_exception( + type(excinfo_.value), + excinfo_.value, + excinfo_.traceback[0]._rawentry, + ) + ) + ) + else: + reprtraceback = self.repr_traceback(excinfo_) + reprcrash = excinfo_._getreprcrash() else: # Fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work. @@ -934,25 +1069,17 @@ class FormattedExcinfo: traceback.format_exception(type(e), e, None) ) reprcrash = None - repr_chain += [(reprtraceback, reprcrash, descr)] + if e.__cause__ is not None and self.chain: e = e.__cause__ - excinfo_ = ( - ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None descr = "The above exception was the direct cause of the following exception:" elif ( e.__context__ is not None and not e.__suppress_context__ and self.chain ): e = e.__context__ - excinfo_ = ( - ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) + excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None descr = "During handling of the above exception, another exception occurred:" else: e = None @@ -960,7 +1087,7 @@ class FormattedExcinfo: return ExceptionChainRepr(repr_chain) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class TerminalRepr: def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception @@ -978,14 +1105,14 @@ class TerminalRepr: # This class is abstract -- only subclasses are instantiated. -@attr.s(eq=False) +@dataclasses.dataclass(eq=False) class ExceptionRepr(TerminalRepr): # Provided by subclasses. - reprcrash: Optional["ReprFileLocation"] reprtraceback: "ReprTraceback" - - def __attrs_post_init__(self) -> None: - self.sections: List[Tuple[str, str, str]] = [] + reprcrash: Optional["ReprFileLocation"] + sections: List[Tuple[str, str, str]] = dataclasses.field( + init=False, default_factory=list + ) def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) @@ -996,16 +1123,23 @@ class ExceptionRepr(TerminalRepr): tw.line(content) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ExceptionChainRepr(ExceptionRepr): chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]] - def __attrs_post_init__(self) -> None: - super().__attrs_post_init__() + def __init__( + self, + chain: Sequence[ + Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] + ], + ) -> None: # reprcrash and reprtraceback of the outermost (the newest) exception # in the chain. - self.reprtraceback = self.chain[-1][0] - self.reprcrash = self.chain[-1][1] + super().__init__( + reprtraceback=chain[-1][0], + reprcrash=chain[-1][1], + ) + self.chain = chain def toterminal(self, tw: TerminalWriter) -> None: for element in self.chain: @@ -1016,21 +1150,21 @@ class ExceptionChainRepr(ExceptionRepr): super().toterminal(tw) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback: "ReprTraceback" - reprcrash: "ReprFileLocation" + reprcrash: Optional["ReprFileLocation"] def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) super().toterminal(tw) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprTraceback(TerminalRepr): reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] extraline: Optional[str] - style: "_TracebackStyle" + style: _TracebackStyle entrysep: ClassVar = "_ " @@ -1055,28 +1189,28 @@ class ReprTraceback(TerminalRepr): class ReprTracebackNative(ReprTraceback): def __init__(self, tblines: Sequence[str]) -> None: - self.style = "native" self.reprentries = [ReprEntryNative(tblines)] self.extraline = None + self.style = "native" -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprEntryNative(TerminalRepr): lines: Sequence[str] - style: ClassVar["_TracebackStyle"] = "native" + style: ClassVar[_TracebackStyle] = "native" def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprEntry(TerminalRepr): lines: Sequence[str] reprfuncargs: Optional["ReprFuncArgs"] reprlocals: Optional["ReprLocals"] reprfileloc: Optional["ReprFileLocation"] - style: "_TracebackStyle" + style: _TracebackStyle def _write_entry_lines(self, tw: TerminalWriter) -> None: """Write the source code portions of a list of traceback entries with syntax highlighting. @@ -1091,7 +1225,6 @@ class ReprEntry(TerminalRepr): the "E" prefix) using syntax highlighting, taking care to not highlighting the ">" character, as doing so might break line continuations. """ - if not self.lines: return @@ -1124,8 +1257,8 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw: TerminalWriter) -> None: if self.style == "short": - assert self.reprfileloc is not None - self.reprfileloc.toterminal(tw) + if self.reprfileloc: + self.reprfileloc.toterminal(tw) self._write_entry_lines(tw) if self.reprlocals: self.reprlocals.toterminal(tw, indent=" " * 8) @@ -1150,12 +1283,15 @@ class ReprEntry(TerminalRepr): ) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprFileLocation(TerminalRepr): - path: str = attr.ib(converter=str) + path: str lineno: int message: str + def __post_init__(self) -> None: + self.path = str(self.path) + def toterminal(self, tw: TerminalWriter) -> None: # Filename and lineno output for each entry, using an output format # that most editors understand. @@ -1167,7 +1303,7 @@ class ReprFileLocation(TerminalRepr): tw.line(f":{self.lineno}: {msg}") -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprLocals(TerminalRepr): lines: Sequence[str] @@ -1176,7 +1312,7 @@ class ReprLocals(TerminalRepr): tw.line(indent + line) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprFuncArgs(TerminalRepr): args: Sequence[Tuple[str, object]] @@ -1211,7 +1347,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: # in 6ec13a2b9. It ("place_as") appears to be something very custom. obj = get_real_func(obj) if hasattr(obj, "place_as"): - obj = obj.place_as # type: ignore[attr-defined] + obj = obj.place_as try: code = Code.from_function(obj) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/source.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/source.py index 208cfb80037..7fa577e03b3 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/source.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_code/source.py @@ -1,10 +1,10 @@ +# mypy: allow-untyped-defs import ast +from bisect import bisect_right import inspect import textwrap import tokenize import types -import warnings -from bisect import bisect_right from typing import Iterable from typing import Iterator from typing import List @@ -12,6 +12,7 @@ from typing import Optional from typing import overload from typing import Tuple from typing import Union +import warnings class Source: @@ -46,12 +47,10 @@ class Source: __hash__ = None # type: ignore @overload - def __getitem__(self, key: int) -> str: - ... + def __getitem__(self, key: int) -> str: ... @overload - def __getitem__(self, key: slice) -> "Source": - ... + def __getitem__(self, key: slice) -> "Source": ... def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: if isinstance(key, int): @@ -149,8 +148,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i values: List[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): - # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. - # Since Python 3.8, the lineno points to the class/def, so need to include the decorators. + # The lineno points to the class/def, so need to include the decorators. if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): for d in x.decorator_list: values.append(d.lineno - 1) @@ -197,7 +195,9 @@ def getstatementrange_ast( # by using the BlockFinder helper used which inspect.getsource() uses itself. block_finder = inspect.BlockFinder() # If we start with an indented line, put blockfinder to "started" mode. - block_finder.started = source.lines[start][0].isspace() + block_finder.started = ( + bool(source.lines[start]) and source.lines[start][0].isspace() + ) it = ((x + "\n") for x in source.lines[start:end]) try: for tok in tokenize.generate_tokens(lambda: next(it)): diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/pprint.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/pprint.py new file mode 100644 index 00000000000..75e9a7123b5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/pprint.py @@ -0,0 +1,676 @@ +# mypy: allow-untyped-defs +# This module was imported from the cpython standard library +# (https://github.com/python/cpython/) at commit +# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). +# +# +# Original Author: Fred L. Drake, Jr. +# fdrake@acm.org +# +# This is a simple little module I wrote to make life easier. I didn't +# see anything quite like it in the library, though I may have overlooked +# something. I wrote this when I was trying to read some heavily nested +# tuples with fairly non-descriptive content. This is modeled very much +# after Lisp/Scheme - style pretty-printing of lists. If you find it +# useful, thank small children who sleep at night. +import collections as _collections +import dataclasses as _dataclasses +from io import StringIO as _StringIO +import re +import types as _types +from typing import Any +from typing import Callable +from typing import Dict +from typing import IO +from typing import Iterator +from typing import List +from typing import Optional +from typing import Set +from typing import Tuple + + +class _safe_key: + """Helper function for key functions when sorting unorderable objects. + + The wrapped-object will fallback to a Py2.x style comparison for + unorderable types (sorting first comparing the type name and then by + the obj ids). Does not work recursively, so dict.items() must have + _safe_key applied to both the key and the value. + + """ + + __slots__ = ["obj"] + + def __init__(self, obj): + self.obj = obj + + def __lt__(self, other): + try: + return self.obj < other.obj + except TypeError: + return (str(type(self.obj)), id(self.obj)) < ( + str(type(other.obj)), + id(other.obj), + ) + + +def _safe_tuple(t): + """Helper function for comparing 2-tuples""" + return _safe_key(t[0]), _safe_key(t[1]) + + +class PrettyPrinter: + def __init__( + self, + indent: int = 4, + width: int = 80, + depth: Optional[int] = None, + ) -> None: + """Handle pretty printing operations onto a stream using a set of + configured parameters. + + indent + Number of spaces to indent for each level of nesting. + + width + Attempted maximum number of columns in the output. + + depth + The maximum depth to print out nested structures. + + """ + if indent < 0: + raise ValueError("indent must be >= 0") + if depth is not None and depth <= 0: + raise ValueError("depth must be > 0") + if not width: + raise ValueError("width must be != 0") + self._depth = depth + self._indent_per_level = indent + self._width = width + + def pformat(self, object: Any) -> str: + sio = _StringIO() + self._format(object, sio, 0, 0, set(), 0) + return sio.getvalue() + + def _format( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + objid = id(object) + if objid in context: + stream.write(_recursion(object)) + return + + p = self._dispatch.get(type(object).__repr__, None) + if p is not None: + context.add(objid) + p(self, object, stream, indent, allowance, context, level + 1) + context.remove(objid) + elif ( + _dataclasses.is_dataclass(object) + and not isinstance(object, type) + and object.__dataclass_params__.repr + and + # Check dataclass has generated repr method. + hasattr(object.__repr__, "__wrapped__") + and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ + ): + context.add(objid) + self._pprint_dataclass( + object, stream, indent, allowance, context, level + 1 + ) + context.remove(objid) + else: + stream.write(self._repr(object, context, level)) + + def _pprint_dataclass( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + cls_name = object.__class__.__name__ + items = [ + (f.name, getattr(object, f.name)) + for f in _dataclasses.fields(object) + if f.repr + ] + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch: Dict[ + Callable[..., str], + Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None], + ] = {} + + def _pprint_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + write = stream.write + write("{") + items = sorted(object.items(), key=_safe_tuple) + self._format_dict_items(items, stream, indent, allowance, context, level) + write("}") + + _dispatch[dict.__repr__] = _pprint_dict + + def _pprint_ordered_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + cls = object.__class__ + stream.write(cls.__name__ + "(") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict + + def _pprint_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + stream.write("[") + self._format_items(object, stream, indent, allowance, context, level) + stream.write("]") + + _dispatch[list.__repr__] = _pprint_list + + def _pprint_tuple( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + stream.write("(") + self._format_items(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[tuple.__repr__] = _pprint_tuple + + def _pprint_set( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not len(object): + stream.write(repr(object)) + return + typ = object.__class__ + if typ is set: + stream.write("{") + endchar = "}" + else: + stream.write(typ.__name__ + "({") + endchar = "})" + object = sorted(object, key=_safe_key) + self._format_items(object, stream, indent, allowance, context, level) + stream.write(endchar) + + _dispatch[set.__repr__] = _pprint_set + _dispatch[frozenset.__repr__] = _pprint_set + + def _pprint_str( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + write = stream.write + if not len(object): + write(repr(object)) + return + chunks = [] + lines = object.splitlines(True) + if level == 1: + indent += 1 + allowance += 1 + max_width1 = max_width = self._width - indent + for i, line in enumerate(lines): + rep = repr(line) + if i == len(lines) - 1: + max_width1 -= allowance + if len(rep) <= max_width1: + chunks.append(rep) + else: + # A list of alternating (non-space, space) strings + parts = re.findall(r"\S*\s*", line) + assert parts + assert not parts[-1] + parts.pop() # drop empty last part + max_width2 = max_width + current = "" + for j, part in enumerate(parts): + candidate = current + part + if j == len(parts) - 1 and i == len(lines) - 1: + max_width2 -= allowance + if len(repr(candidate)) > max_width2: + if current: + chunks.append(repr(current)) + current = part + else: + current = candidate + if current: + chunks.append(repr(current)) + if len(chunks) == 1: + write(rep) + return + if level == 1: + write("(") + for i, rep in enumerate(chunks): + if i > 0: + write("\n" + " " * indent) + write(rep) + if level == 1: + write(")") + + _dispatch[str.__repr__] = _pprint_str + + def _pprint_bytes( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + write = stream.write + if len(object) <= 4: + write(repr(object)) + return + parens = level == 1 + if parens: + indent += 1 + allowance += 1 + write("(") + delim = "" + for rep in _wrap_bytes_repr(object, self._width - indent, allowance): + write(delim) + write(rep) + if not delim: + delim = "\n" + " " * indent + if parens: + write(")") + + _dispatch[bytes.__repr__] = _pprint_bytes + + def _pprint_bytearray( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + write = stream.write + write("bytearray(") + self._pprint_bytes( + bytes(object), stream, indent + 10, allowance + 1, context, level + 1 + ) + write(")") + + _dispatch[bytearray.__repr__] = _pprint_bytearray + + def _pprint_mappingproxy( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + stream.write("mappingproxy(") + self._format(object.copy(), stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy + + def _pprint_simplenamespace( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if type(object) is _types.SimpleNamespace: + # The SimpleNamespace repr is "namespace" instead of the class + # name, so we do the same here. For subclasses; use the class name. + cls_name = "namespace" + else: + cls_name = object.__class__.__name__ + items = object.__dict__.items() + stream.write(cls_name + "(") + self._format_namespace_items(items, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace + + def _format_dict_items( + self, + items: List[Tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(self._repr(key, context, level)) + write(": ") + self._format(ent, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _format_namespace_items( + self, + items: List[Tuple[Any, Any]], + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + for key, ent in items: + write(delimnl) + write(key) + write("=") + if id(ent) in context: + # Special-case representation of recursion to match standard + # recursive dataclass repr. + write("...") + else: + self._format( + ent, + stream, + item_indent + len(key) + 1, + 1, + context, + level, + ) + + write(",") + + write("\n" + " " * indent) + + def _format_items( + self, + items: List[Any], + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not items: + return + + write = stream.write + item_indent = indent + self._indent_per_level + delimnl = "\n" + " " * item_indent + + for item in items: + write(delimnl) + self._format(item, stream, item_indent, 1, context, level) + write(",") + + write("\n" + " " * indent) + + def _repr(self, object: Any, context: Set[int], level: int) -> str: + return self._safe_repr(object, context.copy(), self._depth, level) + + def _pprint_default_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + rdf = self._repr(object.default_factory, context, level) + stream.write(f"{object.__class__.__name__}({rdf}, ") + self._pprint_dict(object, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict + + def _pprint_counter( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + + if object: + stream.write("{") + items = object.most_common() + self._format_dict_items(items, stream, indent, allowance, context, level) + stream.write("}") + + stream.write(")") + + _dispatch[_collections.Counter.__repr__] = _pprint_counter + + def _pprint_chain_map( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])): + stream.write(repr(object)) + return + + stream.write(object.__class__.__name__ + "(") + self._format_items(object.maps, stream, indent, allowance, context, level) + stream.write(")") + + _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map + + def _pprint_deque( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + stream.write(object.__class__.__name__ + "(") + if object.maxlen is not None: + stream.write("maxlen=%d, " % object.maxlen) + stream.write("[") + + self._format_items(object, stream, indent, allowance + 1, context, level) + stream.write("])") + + _dispatch[_collections.deque.__repr__] = _pprint_deque + + def _pprint_user_dict( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict + + def _pprint_user_list( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserList.__repr__] = _pprint_user_list + + def _pprint_user_string( + self, + object: Any, + stream: IO[str], + indent: int, + allowance: int, + context: Set[int], + level: int, + ) -> None: + self._format(object.data, stream, indent, allowance, context, level - 1) + + _dispatch[_collections.UserString.__repr__] = _pprint_user_string + + def _safe_repr( + self, object: Any, context: Set[int], maxlevels: Optional[int], level: int + ) -> str: + typ = type(object) + if typ in _builtin_scalars: + return repr(object) + + r = getattr(typ, "__repr__", None) + + if issubclass(typ, dict) and r is dict.__repr__: + if not object: + return "{}" + objid = id(object) + if maxlevels and level >= maxlevels: + return "{...}" + if objid in context: + return _recursion(object) + context.add(objid) + components: List[str] = [] + append = components.append + level += 1 + for k, v in sorted(object.items(), key=_safe_tuple): + krepr = self._safe_repr(k, context, maxlevels, level) + vrepr = self._safe_repr(v, context, maxlevels, level) + append(f"{krepr}: {vrepr}") + context.remove(objid) + return "{%s}" % ", ".join(components) + + if (issubclass(typ, list) and r is list.__repr__) or ( + issubclass(typ, tuple) and r is tuple.__repr__ + ): + if issubclass(typ, list): + if not object: + return "[]" + format = "[%s]" + elif len(object) == 1: + format = "(%s,)" + else: + if not object: + return "()" + format = "(%s)" + objid = id(object) + if maxlevels and level >= maxlevels: + return format % "..." + if objid in context: + return _recursion(object) + context.add(objid) + components = [] + append = components.append + level += 1 + for o in object: + orepr = self._safe_repr(o, context, maxlevels, level) + append(orepr) + context.remove(objid) + return format % ", ".join(components) + + return repr(object) + + +_builtin_scalars = frozenset( + {str, bytes, bytearray, float, complex, bool, type(None), int} +) + + +def _recursion(object: Any) -> str: + return f"<Recursion on {type(object).__name__} with id={id(object)}>" + + +def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]: + current = b"" + last = len(object) // 4 * 4 + for i in range(0, len(object), 4): + part = object[i : i + 4] + candidate = current + part + if i == last: + width -= allowance + if len(repr(candidate)) > width: + if current: + yield repr(current) + current = part + else: + current = candidate + if current: + yield repr(current) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py index e7ff5cab203..9f33fced676 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/saferepr.py @@ -1,8 +1,5 @@ import pprint import reprlib -from typing import Any -from typing import Dict -from typing import IO from typing import Optional @@ -22,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str: raise except BaseException as exc: exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})" - return "<[{} raised in repr()] {} object at 0x{:x}>".format( - exc_info, type(obj).__name__, id(obj) + return ( + f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>" ) @@ -41,7 +38,7 @@ class SafeRepr(reprlib.Repr): information on exceptions raised during the call. """ - def __init__(self, maxsize: Optional[int]) -> None: + def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None: """ :param maxsize: If not None, will truncate the resulting repr to that specific size, using ellipsis @@ -54,10 +51,15 @@ class SafeRepr(reprlib.Repr): # truncation. self.maxstring = maxsize if maxsize is not None else 1_000_000_000 self.maxsize = maxsize + self.use_ascii = use_ascii def repr(self, x: object) -> str: try: - s = super().repr(x) + if self.use_ascii: + s = ascii(x) + else: + s = super().repr(x) + except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: @@ -94,7 +96,9 @@ def safeformat(obj: object) -> str: DEFAULT_REPR_MAX_SIZE = 240 -def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str: +def saferepr( + obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False +) -> str: """Return a size-limited safe repr-string for the given object. Failing __repr__ functions of user instances will be represented @@ -104,50 +108,23 @@ def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str This function is a wrapper around the Repr/reprlib functionality of the stdlib. """ - return SafeRepr(maxsize).repr(obj) - - -class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): - """PrettyPrinter that always dispatches (regardless of width).""" - - def _format( - self, - object: object, - stream: IO[str], - indent: int, - allowance: int, - context: Dict[int, Any], - level: int, - ) -> None: - # Type ignored because _dispatch is private. - p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined] - - objid = id(object) - if objid in context or p is None: - # Type ignored because _format is private. - super()._format( # type: ignore[misc] - object, - stream, - indent, - allowance, - context, - level, - ) - return - - context[objid] = 1 - p(self, object, stream, indent, allowance, context, level + 1) - del context[objid] - - -def _pformat_dispatch( - object: object, - indent: int = 1, - width: int = 80, - depth: Optional[int] = None, - *, - compact: bool = False, -) -> str: - return AlwaysDispatchingPrettyPrinter( - indent=indent, width=width, depth=depth, compact=compact - ).pformat(object) + return SafeRepr(maxsize, use_ascii).repr(obj) + + +def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: + """Return an unlimited-size safe repr-string for the given object. + + As with saferepr, failing __repr__ functions of user instances + will be represented with a short exception info. + + This function is a wrapper around simple repr. + + Note: a cleaner solution would be to alter ``saferepr``this way + when maxsize=None, but that might affect some other code. + """ + try: + if use_ascii: + return ascii(obj) + return repr(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py index 379035d858c..deb6ecc3c94 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/terminalwriter.py @@ -1,13 +1,16 @@ """Helper functions for writing to terminals and files.""" + import os import shutil import sys +from typing import final +from typing import Literal from typing import Optional from typing import Sequence from typing import TextIO +from ..compat import assert_never from .wcwidth import wcswidth -from _pytest.compat import final # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -28,9 +31,9 @@ def should_do_markup(file: TextIO) -> bool: return True if os.environ.get("PY_COLORS") == "0": return False - if "NO_COLOR" in os.environ: + if os.environ.get("NO_COLOR"): return False - if "FORCE_COLOR" in os.environ: + if os.environ.get("FORCE_COLOR"): return True return ( hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb" @@ -182,9 +185,7 @@ class TerminalWriter: """ if indents and len(indents) != len(lines): raise ValueError( - "indents size ({}) should have same size as lines ({})".format( - len(indents), len(lines) - ) + f"indents size ({len(indents)}) should have same size as lines ({len(lines)})" ) if not indents: indents = [""] * len(lines) @@ -193,15 +194,24 @@ class TerminalWriter: for indent, new_line in zip(indents, new_lines): self.line(indent + new_line) - def _highlight(self, source: str) -> str: - """Highlight the given source code if we have markup support.""" + def _highlight( + self, source: str, lexer: Literal["diff", "python"] = "python" + ) -> str: + """Highlight the given source if we have markup support.""" from _pytest.config.exceptions import UsageError - if not self.hasmarkup or not self.code_highlight: + if not source or not self.hasmarkup or not self.code_highlight: return source + try: from pygments.formatters.terminal import TerminalFormatter - from pygments.lexers.python import PythonLexer + + if lexer == "python": + from pygments.lexers.python import PythonLexer as Lexer + elif lexer == "diff": + from pygments.lexers.diff import DiffLexer as Lexer + else: + assert_never(lexer) from pygments import highlight import pygments.util except ImportError: @@ -210,24 +220,32 @@ class TerminalWriter: try: highlighted: str = highlight( source, - PythonLexer(), + Lexer(), TerminalFormatter( bg=os.getenv("PYTEST_THEME_MODE", "dark"), style=os.getenv("PYTEST_THEME"), ), ) - return highlighted - except pygments.util.ClassNotFound: + # pygments terminal formatter may add a newline when there wasn't one. + # We don't want this, remove. + if highlighted[-1] == "\n" and source[-1] != "\n": + highlighted = highlighted[:-1] + + # Some lexers will not set the initial color explicitly + # which may lead to the previous color being propagated to the + # start of the expression, so reset first. + return "\x1b[0m" + highlighted + except pygments.util.ClassNotFound as e: raise UsageError( "PYTEST_THEME environment variable had an invalid value: '{}'. " "Only valid pygment styles are allowed.".format( os.getenv("PYTEST_THEME") ) - ) - except pygments.util.OptionError: + ) from e + except pygments.util.OptionError as e: raise UsageError( "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. " "The only allowed values are 'dark' and 'light'.".format( os.getenv("PYTEST_THEME_MODE") ) - ) + ) from e diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py index e5c7bf4d868..53803133519 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_io/wcwidth.py @@ -1,5 +1,5 @@ -import unicodedata from functools import lru_cache +import unicodedata @lru_cache(100) diff --git a/tests/wpt/tests/tools/third_party/py/tasks/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/__init__.py index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/py/tasks/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/__init__.py diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/error.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/error.py new file mode 100644 index 00000000000..ab3a4ed318e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/error.py @@ -0,0 +1,111 @@ +"""create errno-specific classes for IO or os calls.""" + +from __future__ import annotations + +import errno +import os +import sys +from typing import Callable +from typing import TYPE_CHECKING +from typing import TypeVar + + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + +R = TypeVar("R") + + +class Error(EnvironmentError): + def __repr__(self) -> str: + return "{}.{} {!r}: {} ".format( + self.__class__.__module__, + self.__class__.__name__, + self.__class__.__doc__, + " ".join(map(str, self.args)), + # repr(self.args) + ) + + def __str__(self) -> str: + s = "[{}]: {}".format( + self.__class__.__doc__, + " ".join(map(str, self.args)), + ) + return s + + +_winerrnomap = { + 2: errno.ENOENT, + 3: errno.ENOENT, + 17: errno.EEXIST, + 18: errno.EXDEV, + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable + 22: errno.ENOTDIR, + 20: errno.ENOTDIR, + 267: errno.ENOTDIR, + 5: errno.EACCES, # anything better? +} + + +class ErrorMaker: + """lazily provides Exception classes for each possible POSIX errno + (as defined per the 'errno' module). All such instances + subclass EnvironmentError. + """ + + _errno2class: dict[int, type[Error]] = {} + + def __getattr__(self, name: str) -> type[Error]: + if name[0] == "_": + raise AttributeError(name) + eno = getattr(errno, name) + cls = self._geterrnoclass(eno) + setattr(self, name, cls) + return cls + + def _geterrnoclass(self, eno: int) -> type[Error]: + try: + return self._errno2class[eno] + except KeyError: + clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,)) + errorcls = type( + clsname, + (Error,), + {"__module__": "py.error", "__doc__": os.strerror(eno)}, + ) + self._errno2class[eno] = errorcls + return errorcls + + def checked_call( + self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs + ) -> R: + """Call a function and raise an errno-exception if applicable.""" + __tracebackhide__ = True + try: + return func(*args, **kwargs) + except Error: + raise + except OSError as value: + if not hasattr(value, "errno"): + raise + errno = value.errno + if sys.platform == "win32": + try: + cls = self._geterrnoclass(_winerrnomap[errno]) + except KeyError: + raise value + else: + # we are not on Windows, or we got a proper OSError + cls = self._geterrnoclass(errno) + + raise cls(f"{func.__name__}{args!r}") + + +_error_maker = ErrorMaker() +checked_call = _error_maker.checked_call + + +def __getattr__(attr: str) -> type[Error]: + return getattr(_error_maker, attr) # type: ignore[no-any-return] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/path.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/path.py new file mode 100644 index 00000000000..9b4ec68950d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_py/path.py @@ -0,0 +1,1477 @@ +# mypy: allow-untyped-defs +"""local path implementation.""" + +from __future__ import annotations + +import atexit +from contextlib import contextmanager +import fnmatch +import importlib.util +import io +import os +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import isabs +from os.path import isdir +from os.path import isfile +from os.path import islink +from os.path import normpath +import posixpath +from stat import S_ISDIR +from stat import S_ISLNK +from stat import S_ISREG +import sys +from typing import Any +from typing import Callable +from typing import cast +from typing import Literal +from typing import overload +from typing import TYPE_CHECKING +import uuid +import warnings + +from . import error + + +# Moved from local.py. +iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") + + +class Checkers: + _depend_on_existence = "exists", "link", "dir", "file" + + def __init__(self, path): + self.path = path + + def dotfile(self): + return self.path.basename.startswith(".") + + def ext(self, arg): + if not arg.startswith("."): + arg = "." + arg + return self.path.ext == arg + + def basename(self, arg): + return self.path.basename == arg + + def basestarts(self, arg): + return self.path.basename.startswith(arg) + + def relto(self, arg): + return self.path.relto(arg) + + def fnmatch(self, arg): + return self.path.fnmatch(arg) + + def endswith(self, arg): + return str(self.path).endswith(arg) + + def _evaluate(self, kw): + from .._code.source import getrawcode + + for name, value in kw.items(): + invert = False + meth = None + try: + meth = getattr(self, name) + except AttributeError: + if name[:3] == "not": + invert = True + try: + meth = getattr(self, name[3:]) + except AttributeError: + pass + if meth is None: + raise TypeError(f"no {name!r} checker available for {self.path!r}") + try: + if getrawcode(meth).co_argcount > 1: + if (not meth(value)) ^ invert: + return False + else: + if bool(value) ^ bool(meth()) ^ invert: + return False + except (error.ENOENT, error.ENOTDIR, error.EBUSY): + # EBUSY feels not entirely correct, + # but its kind of necessary since ENOMEDIUM + # is not accessible in python + for name in self._depend_on_existence: + if name in kw: + if kw.get(name): + return False + name = "not" + name + if name in kw: + if not kw.get(name): + return False + return True + + _statcache: Stat + + def _stat(self) -> Stat: + try: + return self._statcache + except AttributeError: + try: + self._statcache = self.path.stat() + except error.ELOOP: + self._statcache = self.path.lstat() + return self._statcache + + def dir(self): + return S_ISDIR(self._stat().mode) + + def file(self): + return S_ISREG(self._stat().mode) + + def exists(self): + return self._stat() + + def link(self): + st = self.path.lstat() + return S_ISLNK(st.mode) + + +class NeverRaised(Exception): + pass + + +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, str): + fil = FNMatcher(fil) + if isinstance(rec, str): + self.rec: Callable[[LocalPath], bool] = FNMatcher(rec) + elif not hasattr(rec, "__call__") and rec: + self.rec = lambda path: True + else: + self.rec = rec + self.fil = fil + self.ignore = ignore + self.breadthfirst = bf + self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort( + [p for p in entries if p.check(dir=1) and (rec is None or rec(p))] + ) + if not self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + + +class FNMatcher: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, path): + pattern = self.pattern + + if ( + pattern.find(path.sep) == -1 + and iswin32 + and pattern.find(posixpath.sep) != -1 + ): + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posixpath.sep, path.sep) + + if pattern.find(path.sep) == -1: + name = path.basename + else: + name = str(path) # path.strpath # XXX svn? + if not os.path.isabs(pattern): + pattern = "*" + path.sep + pattern + return fnmatch.fnmatch(name, pattern) + + +def map_as_list(func, iter): + return list(map(func, iter)) + + +class Stat: + if TYPE_CHECKING: + + @property + def size(self) -> int: ... + + @property + def mtime(self) -> float: ... + + def __getattr__(self, name: str) -> Any: + return getattr(self._osstatresult, "st_" + name) + + def __init__(self, path, osstatresult): + self.path = path + self._osstatresult = osstatresult + + @property + def owner(self): + if iswin32: + raise NotImplementedError("XXX win32") + import pwd + + entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + @property + def group(self): + """Return group name of file.""" + if iswin32: + raise NotImplementedError("XXX win32") + import grp + + entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore] + return entry[0] + + def isdir(self): + return S_ISDIR(self._osstatresult.st_mode) + + def isfile(self): + return S_ISREG(self._osstatresult.st_mode) + + def islink(self): + self.path.lstat() + return S_ISLNK(self._osstatresult.st_mode) + + +def getuserid(user): + import pwd + + if not isinstance(user, int): + user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore] + return user + + +def getgroupid(group): + import grp + + if not isinstance(group, int): + group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore] + return group + + +class LocalPath: + """Object oriented interface to os.path and other local filesystem + related information. + """ + + class ImportMismatchError(ImportError): + """raised on pyimport() if there is a mismatch of __file__'s""" + + sep = os.sep + + def __init__(self, path=None, expanduser=False): + """Initialize and return a local Path instance. + + Path can be relative to the current directory. + If path is None it defaults to the current working directory. + If expanduser is True, tilde-expansion is performed. + Note that Path instances always carry an absolute path. + Note also that passing in a local path object will simply return + the exact same path object. Use new() to get a new copy. + """ + if path is None: + self.strpath = error.checked_call(os.getcwd) + else: + try: + path = os.fspath(path) + except TypeError: + raise ValueError( + "can only pass None, Path instances " + "or non-empty strings to LocalPath" + ) + if expanduser: + path = os.path.expanduser(path) + self.strpath = abspath(path) + + if sys.platform != "win32": + + def chown(self, user, group, rec=0): + """Change ownership to the given user and group. + user and group may be specified by a number or + by a name. if rec is True change ownership + recursively. + """ + uid = getuserid(user) + gid = getgroupid(group) + if rec: + for x in self.visit(rec=lambda x: x.check(link=0)): + if x.check(link=0): + error.checked_call(os.chown, str(x), uid, gid) + error.checked_call(os.chown, str(self), uid, gid) + + def readlink(self) -> str: + """Return value of a symbolic link.""" + # https://github.com/python/mypy/issues/12278 + return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore] + + def mklinkto(self, oldname): + """Posix style hard link to another name.""" + error.checked_call(os.link, str(oldname), str(self)) + + def mksymlinkto(self, value, absolute=1): + """Create a symbolic link with the given value (pointing to another name).""" + if absolute: + error.checked_call(os.symlink, str(value), self.strpath) + else: + base = self.common(value) + # with posix local paths '/' is always a common base + relsource = self.__class__(value).relto(base) + reldest = self.relto(base) + n = reldest.count(self.sep) + target = self.sep.join(("..",) * n + (relsource,)) + error.checked_call(os.symlink, target, self.strpath) + + def __div__(self, other): + return self.join(os.fspath(other)) + + __truediv__ = __div__ # py3k + + @property + def basename(self): + """Basename part of path.""" + return self._getbyspec("basename")[0] + + @property + def dirname(self): + """Dirname part of path.""" + return self._getbyspec("dirname")[0] + + @property + def purebasename(self): + """Pure base name of the path.""" + return self._getbyspec("purebasename")[0] + + @property + def ext(self): + """Extension of the path (including the '.').""" + return self._getbyspec("ext")[0] + + def read_binary(self): + """Read and return a bytestring from reading the path.""" + with self.open("rb") as f: + return f.read() + + def read_text(self, encoding): + """Read and return a Unicode string from reading the path.""" + with self.open("r", encoding=encoding) as f: + return f.read() + + def read(self, mode="r"): + """Read and return a bytestring from reading the path.""" + with self.open(mode) as f: + return f.read() + + def readlines(self, cr=1): + """Read and return a list of lines from the path. if cr is False, the + newline will be removed from the end of each line.""" + mode = "r" + + if not cr: + content = self.read(mode) + return content.split("\n") + else: + f = self.open(mode) + try: + return f.readlines() + finally: + f.close() + + def load(self): + """(deprecated) return object unpickled from self.read()""" + f = self.open("rb") + try: + import pickle + + return error.checked_call(pickle.load, f) + finally: + f.close() + + def move(self, target): + """Move this path to target.""" + if target.relto(self): + raise error.EINVAL(target, "cannot move path into a subdirectory of itself") + try: + self.rename(target) + except error.EXDEV: # invalid cross-device link + self.copy(target) + self.remove() + + def fnmatch(self, pattern): + """Return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + + def relto(self, relpath): + """Return a string which is the relative part of the path + to the given 'relpath'. + """ + if not isinstance(relpath, (str, LocalPath)): + raise TypeError(f"{relpath!r}: not a string or path object") + strrelpath = str(relpath) + if strrelpath and strrelpath[-1] != self.sep: + strrelpath += self.sep + # assert strrelpath[-1] == self.sep + # assert strrelpath[-2] != self.sep + strself = self.strpath + if sys.platform == "win32" or getattr(os, "_name", None) == "nt": + if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)): + return strself[len(strrelpath) :] + elif strself.startswith(strrelpath): + return strself[len(strrelpath) :] + return "" + + def ensure_dir(self, *args): + """Ensure the path joined with args is a directory.""" + return self.ensure(*args, dir=True) + + def bestrelpath(self, dest): + """Return a string which is a relative path from self + (assumed to be a directory) to dest such that + self.join(bestrelpath) == dest and if not such + path can be determined return dest. + """ + try: + if self == dest: + return os.curdir + base = self.common(dest) + if not base: # can be the case on windows + return str(dest) + self2base = self.relto(base) + reldest = dest.relto(base) + if self2base: + n = self2base.count(self.sep) + 1 + else: + n = 0 + lst = [os.pardir] * n + if reldest: + lst.append(reldest) + target = dest.sep.join(lst) + return target + except AttributeError: + return str(dest) + + def exists(self): + return self.check() + + def isdir(self): + return self.check(dir=1) + + def isfile(self): + return self.check(file=1) + + def parts(self, reverse=False): + """Return a root-first list of all ancestor directories + plus the path itself. + """ + current = self + lst = [self] + while 1: + last = current + current = current.dirpath() + if last == current: + break + lst.append(current) + if not reverse: + lst.reverse() + return lst + + def common(self, other): + """Return the common part shared with the other path + or None if there is no common part. + """ + last = None + for x, y in zip(self.parts(), other.parts()): + if x != y: + return last + last = x + return last + + def __add__(self, other): + """Return new path object with 'other' added to the basename""" + return self.new(basename=self.basename + str(other)) + + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): + """Yields all paths below the current one + + fil is a filter (glob pattern or callable), if not matching the + path will not be yielded, defaulting to None (everything is + returned) + + rec is a filter (glob pattern or callable) that controls whether + a node is descended, defaulting to None + + ignore is an Exception class that is ignoredwhen calling dirlist() + on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. + """ + yield from Visitor(fil, rec, ignore, bf, sort).gen(self) + + def _sortlist(self, res, sort): + if sort: + if hasattr(sort, "__call__"): + warnings.warn( + DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), + stacklevel=3, + ) + res.sort(sort) + else: + res.sort() + + def __fspath__(self): + return self.strpath + + def __hash__(self): + s = self.strpath + if iswin32: + s = s.lower() + return hash(s) + + def __eq__(self, other): + s1 = os.fspath(self) + try: + s2 = os.fspath(other) + except TypeError: + return False + if iswin32: + s1 = s1.lower() + try: + s2 = s2.lower() + except AttributeError: + return False + return s1 == s2 + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return os.fspath(self) < os.fspath(other) + + def __gt__(self, other): + return os.fspath(self) > os.fspath(other) + + def samefile(self, other): + """Return True if 'other' references the same file as 'self'.""" + other = os.fspath(other) + if not isabs(other): + other = abspath(other) + if self == other: + return True + if not hasattr(os.path, "samefile"): + return False + return error.checked_call(os.path.samefile, self.strpath, other) + + def remove(self, rec=1, ignore_errors=False): + """Remove a file or directory (or a directory tree if rec=1). + if ignore_errors is True, errors while removing directories will + be ignored. + """ + if self.check(dir=1, link=0): + if rec: + # force remove of readonly files on windows + if iswin32: + self.chmod(0o700, rec=1) + import shutil + + error.checked_call( + shutil.rmtree, self.strpath, ignore_errors=ignore_errors + ) + else: + error.checked_call(os.rmdir, self.strpath) + else: + if iswin32: + self.chmod(0o700) + error.checked_call(os.remove, self.strpath) + + def computehash(self, hashtype="md5", chunksize=524288): + """Return hexdigest of hashvalue for this file.""" + try: + try: + import hashlib as mod + except ImportError: + if hashtype == "sha1": + hashtype = "sha" + mod = __import__(hashtype) + hash = getattr(mod, hashtype)() + except (AttributeError, ImportError): + raise ValueError(f"Don't know how to compute {hashtype!r} hash") + f = self.open("rb") + try: + while 1: + buf = f.read(chunksize) + if not buf: + return hash.hexdigest() + hash.update(buf) + finally: + f.close() + + def new(self, **kw): + """Create a modified version of this path. + the following keyword arguments modify various path parts:: + + a:/some/path/to/a/file.ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext + """ + obj = object.__new__(self.__class__) + if not kw: + obj.strpath = self.strpath + return obj + drive, dirname, basename, purebasename, ext = self._getbyspec( + "drive,dirname,basename,purebasename,ext" + ) + if "basename" in kw: + if "purebasename" in kw or "ext" in kw: + raise ValueError("invalid specification %r" % kw) + else: + pb = kw.setdefault("purebasename", purebasename) + try: + ext = kw["ext"] + except KeyError: + pass + else: + if ext and not ext.startswith("."): + ext = "." + ext + kw["basename"] = pb + ext + + if "dirname" in kw and not kw["dirname"]: + kw["dirname"] = drive + else: + kw.setdefault("dirname", dirname) + kw.setdefault("sep", self.sep) + obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw)) + return obj + + def _getbyspec(self, spec: str) -> list[str]: + """See new for what 'spec' can be.""" + res = [] + parts = self.strpath.split(self.sep) + + args = filter(None, spec.split(",")) + for name in args: + if name == "drive": + res.append(parts[0]) + elif name == "dirname": + res.append(self.sep.join(parts[:-1])) + else: + basename = parts[-1] + if name == "basename": + res.append(basename) + else: + i = basename.rfind(".") + if i == -1: + purebasename, ext = basename, "" + else: + purebasename, ext = basename[:i], basename[i:] + if name == "purebasename": + res.append(purebasename) + elif name == "ext": + res.append(ext) + else: + raise ValueError("invalid part specification %r" % name) + return res + + def dirpath(self, *args, **kwargs): + """Return the directory path joined with any given path arguments.""" + if not kwargs: + path = object.__new__(self.__class__) + path.strpath = dirname(self.strpath) + if args: + path = path.join(*args) + return path + return self.new(basename="").join(*args, **kwargs) + + def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath: + """Return a new path by appending all 'args' as path + components. if abs=1 is used restart from root if any + of the args is an absolute path. + """ + sep = self.sep + strargs = [os.fspath(arg) for arg in args] + strpath = self.strpath + if abs: + newargs: list[str] = [] + for arg in reversed(strargs): + if isabs(arg): + strpath = arg + strargs = newargs + break + newargs.insert(0, arg) + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep + for arg in strargs: + arg = arg.strip(sep) + if iswin32: + # allow unix style paths even on windows. + arg = arg.strip("/") + arg = arg.replace("/", sep) + strpath = strpath + actual_sep + arg + actual_sep = sep + obj = object.__new__(self.__class__) + obj.strpath = normpath(strpath) + return obj + + def open(self, mode="r", ensure=False, encoding=None): + """Return an opened file with the given mode. + + If ensure is True, create parent directories if needed. + """ + if ensure: + self.dirpath().ensure(dir=1) + if encoding: + return error.checked_call( + io.open, + self.strpath, + mode, + encoding=encoding, + ) + return error.checked_call(open, self.strpath, mode) + + def _fastjoin(self, name): + child = object.__new__(self.__class__) + child.strpath = self.strpath + self.sep + name + return child + + def islink(self): + return islink(self.strpath) + + def check(self, **kw): + """Check a path for existence and properties. + + Without arguments, return True if the path exists, otherwise False. + + valid checkers:: + + file = 1 # is a file + file = 0 # is not a file (may not even exist) + dir = 1 # is a dir + link = 1 # is a link + exists = 1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file + """ + if not kw: + return exists(self.strpath) + if len(kw) == 1: + if "dir" in kw: + return not kw["dir"] ^ isdir(self.strpath) + if "file" in kw: + return not kw["file"] ^ isfile(self.strpath) + if not kw: + kw = {"exists": 1} + return Checkers(self)._evaluate(kw) + + _patternchars = set("*?[" + os.sep) + + def listdir(self, fil=None, sort=None): + """List directory contents, possibly filter by the given fil func + and possibly sorted. + """ + if fil is None and sort is None: + names = error.checked_call(os.listdir, self.strpath) + return map_as_list(self._fastjoin, names) + if isinstance(fil, str): + if not self._patternchars.intersection(fil): + child = self._fastjoin(fil) + if exists(child.strpath): + return [child] + return [] + fil = FNMatcher(fil) + names = error.checked_call(os.listdir, self.strpath) + res = [] + for name in names: + child = self._fastjoin(name) + if fil is None or fil(child): + res.append(child) + self._sortlist(res, sort) + return res + + def size(self) -> int: + """Return size of the underlying file object""" + return self.stat().size + + def mtime(self) -> float: + """Return last modification time of the path.""" + return self.stat().mtime + + def copy(self, target, mode=False, stat=False): + """Copy path to target. + + If mode is True, will copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. + """ + if self.check(file=1): + if target.check(dir=1): + target = target.join(self.basename) + assert self != target + copychunked(self, target) + if mode: + copymode(self.strpath, target.strpath) + if stat: + copystat(self, target) + else: + + def rec(p): + return p.check(link=0) + + for x in self.visit(rec=rec): + relpath = x.relto(self) + newx = target.join(relpath) + newx.dirpath().ensure(dir=1) + if x.check(link=1): + newx.mksymlinkto(x.readlink()) + continue + elif x.check(file=1): + copychunked(x, newx) + elif x.check(dir=1): + newx.ensure(dir=1) + if mode: + copymode(x.strpath, newx.strpath) + if stat: + copystat(x, newx) + + def rename(self, target): + """Rename this path to target.""" + target = os.fspath(target) + return error.checked_call(os.rename, self.strpath, target) + + def dump(self, obj, bin=1): + """Pickle object into path location""" + f = self.open("wb") + import pickle + + try: + error.checked_call(pickle.dump, obj, f, bin) + finally: + f.close() + + def mkdir(self, *args): + """Create & return the directory joined with args.""" + p = self.join(*args) + error.checked_call(os.mkdir, os.fspath(p)) + return p + + def write_binary(self, data, ensure=False): + """Write binary data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("wb") as f: + f.write(data) + + def write_text(self, data, encoding, ensure=False): + """Write text data into path using the specified encoding. + If ensure is True create missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + with self.open("w", encoding=encoding) as f: + f.write(data) + + def write(self, data, mode="w", ensure=False): + """Write data into path. If ensure is True create + missing parent directories. + """ + if ensure: + self.dirpath().ensure(dir=1) + if "b" in mode: + if not isinstance(data, bytes): + raise ValueError("can only process bytes") + else: + if not isinstance(data, str): + if not isinstance(data, bytes): + data = str(data) + else: + data = data.decode(sys.getdefaultencoding()) + f = self.open(mode) + try: + f.write(data) + finally: + f.close() + + def _ensuredirs(self): + parent = self.dirpath() + if parent == self: + return self + if parent.check(dir=0): + parent._ensuredirs() + if self.check(dir=0): + try: + self.mkdir() + except error.EEXIST: + # race condition: file/dir created by another thread/process. + # complain if it is not a dir + if self.check(dir=0): + raise + return self + + def ensure(self, *args, **kwargs): + """Ensure that an args-joined path exists (by default as + a file). if you specify a keyword argument 'dir=True' + then the path is forced to be a directory path. + """ + p = self.join(*args) + if kwargs.get("dir", 0): + return p._ensuredirs() + else: + p.dirpath()._ensuredirs() + if not p.check(file=1): + p.open("wb").close() + return p + + @overload + def stat(self, raising: Literal[True] = ...) -> Stat: ... + + @overload + def stat(self, raising: Literal[False]) -> Stat | None: ... + + def stat(self, raising: bool = True) -> Stat | None: + """Return an os.stat() tuple.""" + if raising: + return Stat(self, error.checked_call(os.stat, self.strpath)) + try: + return Stat(self, os.stat(self.strpath)) + except KeyboardInterrupt: + raise + except Exception: + return None + + def lstat(self) -> Stat: + """Return an os.lstat() tuple.""" + return Stat(self, error.checked_call(os.lstat, self.strpath)) + + def setmtime(self, mtime=None): + """Set modification time for the given path. if 'mtime' is None + (the default) then the file's mtime is set to current time. + + Note that the resolution for 'mtime' is platform dependent. + """ + if mtime is None: + return error.checked_call(os.utime, self.strpath, mtime) + try: + return error.checked_call(os.utime, self.strpath, (-1, mtime)) + except error.EINVAL: + return error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) + + def chdir(self): + """Change directory to self and return old current directory""" + try: + old = self.__class__() + except error.ENOENT: + old = None + error.checked_call(os.chdir, self.strpath) + return old + + @contextmanager + def as_cwd(self): + """ + Return a context manager, which changes to the path's dir during the + managed "with" context. + On __enter__ it returns the old dir, which might be ``None``. + """ + old = self.chdir() + try: + yield old + finally: + if old is not None: + old.chdir() + + def realpath(self): + """Return a new path which contains no symbolic links.""" + return self.__class__(os.path.realpath(self.strpath)) + + def atime(self): + """Return last access time of the path.""" + return self.stat().atime + + def __repr__(self): + return "local(%r)" % self.strpath + + def __str__(self): + """Return string representation of the Path.""" + return self.strpath + + def chmod(self, mode, rec=0): + """Change permissions to the given mode. If mode is an + integer it directly encodes the os-specific modes. + if rec is True perform recursively. + """ + if not isinstance(mode, int): + raise TypeError(f"mode {mode!r} must be an integer") + if rec: + for x in self.visit(rec=rec): + error.checked_call(os.chmod, str(x), mode) + error.checked_call(os.chmod, self.strpath, mode) + + def pypkgpath(self): + """Return the Python package path by looking for the last + directory upwards which still contains an __init__.py. + Return None if a pkgpath cannot be determined. + """ + pkgpath = None + for parent in self.parts(reverse=True): + if parent.isdir(): + if not parent.join("__init__.py").exists(): + break + if not isimportable(parent.basename): + break + pkgpath = parent + return pkgpath + + def _ensuresyspath(self, ensuremode, path): + if ensuremode: + s = str(path) + if ensuremode == "append": + if s not in sys.path: + sys.path.append(s) + else: + if s != sys.path[0]: + sys.path.insert(0, s) + + def pyimport(self, modname=None, ensuresyspath=True): + """Return path as an imported python module. + + If modname is None, look for the containing package + and construct an according module name. + The module will be put/looked up in sys.modules. + if ensuresyspath is True then the root dir for importing + the file (taking __init__.py files into account) will + be prepended to sys.path if it isn't there already. + If ensuresyspath=="append" the root dir will be appended + if it isn't already contained in sys.path. + if ensuresyspath is False no modification of syspath happens. + + Special value of ensuresyspath=="importlib" is intended + purely for using in pytest, it is capable only of importing + separate .py files outside packages, e.g. for test suite + without any __init__.py file. It effectively allows having + same-named test modules in different places and offers + mild opt-in via this option. Note that it works only in + recent versions of python. + """ + if not self.check(): + raise error.ENOENT(self) + + if ensuresyspath == "importlib": + if modname is None: + modname = self.purebasename + spec = importlib.util.spec_from_file_location(modname, str(self)) + if spec is None or spec.loader is None: + raise ImportError(f"Can't find module {modname} at location {self!s}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + pkgpath = None + if modname is None: + pkgpath = self.pypkgpath() + if pkgpath is not None: + pkgroot = pkgpath.dirpath() + names = self.new(ext="").relto(pkgroot).split(self.sep) + if names[-1] == "__init__": + names.pop() + modname = ".".join(names) + else: + pkgroot = self.dirpath() + modname = self.purebasename + + self._ensuresyspath(ensuresyspath, pkgroot) + __import__(modname) + mod = sys.modules[modname] + if self.basename == "__init__.py": + return mod # we don't check anything as we might + # be in a namespace package ... too icky to check + modfile = mod.__file__ + assert modfile is not None + if modfile[-4:] in (".pyc", ".pyo"): + modfile = modfile[:-1] + elif modfile.endswith("$py.class"): + modfile = modfile[:-9] + ".py" + if modfile.endswith(os.sep + "__init__.py"): + if self.basename != "__init__.py": + modfile = modfile[:-12] + try: + issame = self.samefile(modfile) + except error.ENOENT: + issame = False + if not issame: + ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH") + if ignore != "1": + raise self.ImportMismatchError(modname, modfile, self) + return mod + else: + try: + return sys.modules[modname] + except KeyError: + # we have a custom modname, do a pseudo-import + import types + + mod = types.ModuleType(modname) + mod.__file__ = str(self) + sys.modules[modname] = mod + try: + with open(str(self), "rb") as f: + exec(f.read(), mod.__dict__) + except BaseException: + del sys.modules[modname] + raise + return mod + + def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str: + """Return stdout text from executing a system child process, + where the 'self' path points to executable. + The process is directly invoked and not through a system shell. + """ + from subprocess import PIPE + from subprocess import Popen + + popen_opts.pop("stdout", None) + popen_opts.pop("stderr", None) + proc = Popen( + [str(self)] + [str(arg) for arg in argv], + **popen_opts, + stdout=PIPE, + stderr=PIPE, + ) + stdout: str | bytes + stdout, stderr = proc.communicate() + ret = proc.wait() + if isinstance(stdout, bytes): + stdout = stdout.decode(sys.getdefaultencoding()) + if ret != 0: + if isinstance(stderr, bytes): + stderr = stderr.decode(sys.getdefaultencoding()) + raise RuntimeError( + ret, + ret, + str(self), + stdout, + stderr, + ) + return stdout + + @classmethod + def sysfind(cls, name, checker=None, paths=None): + """Return a path object found by looking at the systems + underlying PATH specification. If the checker is not None + it will be invoked to filter matching paths. If a binary + cannot be found, None is returned + Note: This is probably not working on plain win32 systems + but may work on cygwin. + """ + if isabs(name): + p = local(name) + if p.check(file=1): + return p + else: + if paths is None: + if iswin32: + paths = os.environ["Path"].split(";") + if "" not in paths and "." not in paths: + paths.append(".") + try: + systemroot = os.environ["SYSTEMROOT"] + except KeyError: + pass + else: + paths = [ + path.replace("%SystemRoot%", systemroot) for path in paths + ] + else: + paths = os.environ["PATH"].split(":") + tryadd = [] + if iswin32: + tryadd += os.environ["PATHEXT"].split(os.pathsep) + tryadd.append("") + + for x in paths: + for addext in tryadd: + p = local(x).join(name, abs=True) + addext + try: + if p.check(file=1): + if checker: + if not checker(p): + continue + return p + except error.EACCES: + pass + return None + + @classmethod + def _gethomedir(cls): + try: + x = os.environ["HOME"] + except KeyError: + try: + x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"] + except KeyError: + return None + return cls(x) + + # """ + # special class constructors for local filesystem paths + # """ + @classmethod + def get_temproot(cls): + """Return the system's temporary directory + (where tempfiles are usually created in) + """ + import tempfile + + return local(tempfile.gettempdir()) + + @classmethod + def mkdtemp(cls, rootdir=None): + """Return a Path object pointing to a fresh new temporary directory + (which we created ourselves). + """ + import tempfile + + if rootdir is None: + rootdir = cls.get_temproot() + path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir)) + return cls(path) + + @classmethod + def make_numbered_dir( + cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800 + ): # two days + """Return unique directory with a number greater than the current + maximum one. The number is assumed to start directly after prefix. + if keep is true directories with a number less than (maxnum-keep) + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. + """ + if rootdir is None: + rootdir = cls.get_temproot() + + nprefix = prefix.lower() + + def parse_num(path): + """Parse the number out of a path (if it matches the prefix)""" + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): + try: + return int(nbasename[len(nprefix) :]) + except ValueError: + pass + + def create_lockfile(path): + """Exclusively create lockfile. Throws when failed""" + mypid = os.getpid() + lockfile = path.join(".lock") + if hasattr(lockfile, "mksymlinkto"): + lockfile.mksymlinkto(str(mypid)) + else: + fd = error.checked_call( + os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644 + ) + with os.fdopen(fd, "w") as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """Ensure lockfile is removed at process exit""" + mypid = os.getpid() + + def try_remove_lockfile(): + # in a fork() situation, only the last process should + # remove the .lock, otherwise the other processes run the + # risk of seeing their temporary dir disappear. For now + # we remove the .lock in the parent only (i.e. we assume + # that the children finish before the parent). + if os.getpid() != mypid: + return + try: + lockfile.remove() + except error.Error: + pass + + atexit.register(try_remove_lockfile) + + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum + 1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (error.EEXIST, error.ENOENT, error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """Read file modification time""" + try: + return path.lstat().mtime + except error.Error: + pass + + garbage_prefix = prefix + "garbage-" + + def is_garbage(path): + """Check if path denotes directory scheduled for removal""" + bn = path.basename + return bn.startswith(garbage_prefix) + + # prune old directories + udir_time = get_mtime(udir) + if keep and udir_time: + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None and num <= (maxnum - keep): + try: + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (error.EEXIST, error.ENOENT, error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + if is_garbage(path): + try: + path.remove(rec=1) + except KeyboardInterrupt: + raise + except Exception: # this might be error.Error, WindowsError ... + pass + + # make link... + try: + username = os.environ["USER"] # linux, et al + except KeyError: + try: + username = os.environ["USERNAME"] # windows + except KeyError: + username = "current" + + src = str(udir) + dest = src[: src.rfind("-")] + "-" + username + try: + os.unlink(dest) + except OSError: + pass + try: + os.symlink(src, dest) + except (OSError, AttributeError, NotImplementedError): + pass + + return udir + + +def copymode(src, dest): + """Copy permission from src to dst.""" + import shutil + + shutil.copymode(src, dest) + + +def copystat(src, dest): + """Copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + + shutil.copystat(str(src), str(dest)) + + +def copychunked(src, dest): + chunksize = 524288 # half a meg of bytes + fsrc = src.open("rb") + try: + fdest = dest.open("wb") + try: + while 1: + buf = fsrc.read(chunksize) + if not buf: + break + fdest.write(buf) + finally: + fdest.close() + finally: + fsrc.close() + + +def isimportable(name): + if name and (name[0].isalpha() or name[0] == "_"): + name = name.replace("_", "") + return not name or name.isalnum() + + +local = LocalPath diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_version.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_version.py index 5515abadad7..1df004d997c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_version.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/_version.py @@ -1,5 +1,5 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = '7.0.1' -version_tuple = (7, 0, 1) +version = '8.2.1' +version_tuple = (8, 2, 1) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py index 480a26ad867..21dd4a4a4bb 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/__init__.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Support for presenting detailed information in failing assertions.""" + import sys from typing import Any from typing import Generator @@ -15,6 +17,7 @@ from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.nodes import Item + if TYPE_CHECKING: from _pytest.main import Session @@ -39,9 +42,17 @@ def pytest_addoption(parser: Parser) -> None: "enable_assertion_pass_hook", type="bool", default=False, - help="Enables the pytest_assertion_pass hook." + help="Enables the pytest_assertion_pass hook. " "Make sure to delete any previously generated pyc cache files.", ) + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_ASSERTIONS, + help=( + "Specify a verbosity level for assertions, overriding the main level. " + "Higher levels will provide more detailed explanation when an assertion fails." + ), + ) def register_assert_rewrite(*names: str) -> None: @@ -53,7 +64,7 @@ def register_assert_rewrite(*names: str) -> None: actually imported, usually in your __init__.py if you are a plugin using a package. - :raises TypeError: If the given module names are not strings. + :param names: The module names to register. """ for name in names: if not isinstance(name, str): @@ -112,15 +123,14 @@ def pytest_collection(session: "Session") -> None: assertstate.hook.set_session(session) -@hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. The rewrite module will use util._reprcompare if it exists to use custom reporting via the pytest_assertrepr_compare hook. This sets up this custom comparison for the test. """ - ihook = item.ihook def callbinrepr(op, left: object, right: object) -> Optional[str]: @@ -162,10 +172,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: util._assertion_pass = call_assertion_pass_hook - yield - - util._reprcompare, util._assertion_pass = saved_assert_hooks - util._config = None + try: + return (yield) + finally: + util._reprcompare, util._assertion_pass = saved_assert_hooks + util._config = None def pytest_sessionfinish(session: "Session") -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py index 88ac6cab368..1e722f2ba15 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/rewrite.py @@ -1,5 +1,7 @@ """Rewrite assertion AST to produce nice error messages.""" + import ast +from collections import defaultdict import errno import functools import importlib.abc @@ -9,12 +11,12 @@ import io import itertools import marshal import os +from pathlib import Path +from pathlib import PurePath import struct import sys import tokenize import types -from pathlib import Path -from pathlib import PurePath from typing import Callable from typing import Dict from typing import IO @@ -32,27 +34,35 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util -from _pytest.assertion.util import ( # noqa: F401 - format_explanation as _format_explanation, -) from _pytest.config import Config from _pytest.main import Session from _pytest.pathlib import absolutepath from _pytest.pathlib import fnmatch_ex from _pytest.stash import StashKey + +# fmt: off +from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip +# fmt:on + if TYPE_CHECKING: from _pytest.assertion import AssertionState -assertstate_key = StashKey["AssertionState"]() +class Sentinel: + pass + +assertstate_key = StashKey["AssertionState"]() # pytest caches rewritten pycs in pycache dirs PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT +# Special marker that denotes we have just left a scope definition +_SCOPE_END_MARKER = Sentinel() + class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): """PEP302/PEP451 import hook which rewrites asserts.""" @@ -100,9 +110,6 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) spec is None # this is a namespace package (without `__init__.py`) # there's nothing to rewrite there - # python3.6: `namespace` - # python3.7+: `None` - or spec.origin == "namespace" or spec.origin is None # we can only rewrite source files or not isinstance(spec.loader, importlib.machinery.SourceFileLoader) @@ -183,7 +190,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) for initial_path in self.session._initialpaths: # Make something as c:/projects/my_project/path.py -> # ['c:', 'projects', 'my_project', 'path.py'] - parts = str(initial_path).split(os.path.sep) + parts = str(initial_path).split(os.sep) # add 'path' to basenames to be checked. self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) @@ -193,7 +200,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) return False # For matching the name it must be as if it was a filename. - path = PurePath(os.path.sep.join(parts) + ".py") + path = PurePath(*parts).with_suffix(".py") for pat in self.fnpats: # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based @@ -277,8 +284,12 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) return f.read() if sys.version_info >= (3, 10): + if sys.version_info >= (3, 12): + from importlib.resources.abc import TraversableResources + else: + from importlib.abc import TraversableResources - def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore + def get_resource_reader(self, name: str) -> TraversableResources: if sys.version_info < (3, 11): from importlib.readers import FileReader else: @@ -295,9 +306,8 @@ def _write_pyc_fp( # import. However, there's little reason to deviate. fp.write(importlib.util.MAGIC_NUMBER) # https://www.python.org/dev/peps/pep-0552/ - if sys.version_info >= (3, 7): - flags = b"\x00\x00\x00\x00" - fp.write(flags) + flags = b"\x00\x00\x00\x00" + fp.write(flags) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF size = source_stat.st_size & 0xFFFFFFFF @@ -306,54 +316,29 @@ def _write_pyc_fp( fp.write(marshal.dumps(co)) -if sys.platform == "win32": - from atomicwrites import atomic_write - - def _write_pyc( - state: "AssertionState", - co: types.CodeType, - source_stat: os.stat_result, - pyc: Path, - ) -> bool: - try: - with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp: - _write_pyc_fp(fp, source_stat, co) - except OSError as e: - state.trace(f"error writing pyc file at {pyc}: {e}") - # we ignore any failure to write the cache file - # there are many reasons, permission-denied, pycache dir being a - # file etc. - return False - return True - - -else: - - def _write_pyc( - state: "AssertionState", - co: types.CodeType, - source_stat: os.stat_result, - pyc: Path, - ) -> bool: - proc_pyc = f"{pyc}.{os.getpid()}" - try: - fp = open(proc_pyc, "wb") - except OSError as e: - state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") - return False - - try: +def _write_pyc( + state: "AssertionState", + co: types.CodeType, + source_stat: os.stat_result, + pyc: Path, +) -> bool: + proc_pyc = f"{pyc}.{os.getpid()}" + try: + with open(proc_pyc, "wb") as fp: _write_pyc_fp(fp, source_stat, co) - os.rename(proc_pyc, pyc) - except OSError as e: - state.trace(f"error writing pyc file at {pyc}: {e}") - # we ignore any failure to write the cache file - # there are many reasons, permission-denied, pycache dir being a - # file etc. - return False - finally: - fp.close() - return True + except OSError as e: + state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") + return False + + try: + os.replace(proc_pyc, pyc) + except OSError as e: + state.trace(f"error writing pyc file at {pyc}: {e}") + # we ignore any failure to write the cache file + # there are many reasons, permission-denied, pycache dir being a + # file etc. + return False + return True def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: @@ -379,31 +364,29 @@ def _read_pyc( except OSError: return None with fp: - # https://www.python.org/dev/peps/pep-0552/ - has_flags = sys.version_info >= (3, 7) try: stat_result = os.stat(source) mtime = int(stat_result.st_mtime) size = stat_result.st_size - data = fp.read(16 if has_flags else 12) + data = fp.read(16) except OSError as e: trace(f"_read_pyc({source}): OSError {e}") return None # Check for invalid or out of date pyc file. - if len(data) != (16 if has_flags else 12): + if len(data) != (16): trace("_read_pyc(%s): invalid pyc (too short)" % source) return None if data[:4] != importlib.util.MAGIC_NUMBER: trace("_read_pyc(%s): invalid pyc (bad magic number)" % source) return None - if has_flags and data[4:8] != b"\x00\x00\x00\x00": + if data[4:8] != b"\x00\x00\x00\x00": trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source) return None - mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8] + mtime_data = data[8:12] if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF: trace("_read_pyc(%s): out of date" % source) return None - size_data = data[12 if has_flags else 8 : 16 if has_flags else 12] + size_data = data[12:16] if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: trace("_read_pyc(%s): invalid pyc (incorrect size)" % source) return None @@ -444,7 +427,10 @@ def _saferepr(obj: object) -> str: def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]: """Get `maxsize` configuration for saferepr based on the given config object.""" - verbosity = config.getoption("verbose") if config is not None else 0 + if config is None: + verbosity = 0 + else: + verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) if verbosity >= 2: return None if verbosity >= 1: @@ -598,7 +584,7 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]: # multi-line assert with message elif lineno in seen_lines: lines[-1] = lines[-1][:offset] - # multi line assert with escapd newline before message + # multi line assert with escaped newline before message else: lines.append(line[:offset]) _write_and_reset() @@ -660,8 +646,14 @@ class AssertionRewriter(ast.NodeVisitor): .push_format_context() and .pop_format_context() which allows to build another %-formatted string while already building one. - This state is reset on every new assert statement visited and used - by the other visitors. + :scope: A tuple containing the current scope used for variables_overwrite. + + :variables_overwrite: A dict filled with references to variables + that change value within an assert. This happens when a variable is + reassigned with the walrus operator + + This state, except the variables_overwrite, is reset on every new assert + statement visited and used by the other visitors. """ def __init__( @@ -677,6 +669,10 @@ class AssertionRewriter(ast.NodeVisitor): else: self.enable_assertion_pass_hook = False self.source = source + self.scope: tuple[ast.AST, ...] = () + self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = ( + defaultdict(dict) + ) def run(self, mod: ast.Module) -> None: """Find all assert statements in *mod* and rewrite them.""" @@ -691,14 +687,15 @@ class AssertionRewriter(ast.NodeVisitor): if doc is not None and self.is_rewrite_disabled(doc): return pos = 0 - lineno = 1 + item = None for item in mod.body: if ( expect_docstring and isinstance(item, ast.Expr) - and isinstance(item.value, ast.Str) + and isinstance(item.value, ast.Constant) + and isinstance(item.value.value, str) ): - doc = item.value.s + doc = item.value.value if self.is_rewrite_disabled(doc): return expect_docstring = False @@ -739,9 +736,17 @@ class AssertionRewriter(ast.NodeVisitor): mod.body[pos:pos] = imports # Collect asserts. - nodes: List[ast.AST] = [mod] + self.scope = (mod,) + nodes: List[Union[ast.AST, Sentinel]] = [mod] while nodes: node = nodes.pop() + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + self.scope = tuple((*self.scope, node)) + nodes.append(_SCOPE_END_MARKER) + if node == _SCOPE_END_MARKER: + self.scope = self.scope[:-1] + continue + assert isinstance(node, ast.AST) for name, field in ast.iter_fields(node): if isinstance(field, list): new: List[ast.AST] = [] @@ -830,7 +835,7 @@ class AssertionRewriter(ast.NodeVisitor): current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys = [ast.Str(key) for key in current.keys()] + keys = [ast.Constant(key) for key in current.keys()] format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) @@ -854,9 +859,10 @@ class AssertionRewriter(ast.NodeVisitor): the expression is false. """ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: - from _pytest.warning_types import PytestAssertRewriteWarning import warnings + from _pytest.warning_types import PytestAssertRewriteWarning + # TODO: This assert should not be needed. assert self.module_path is not None warnings.warn_explicit( @@ -884,16 +890,16 @@ class AssertionRewriter(ast.NodeVisitor): negation = ast.UnaryOp(ast.Not(), top_condition) if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - msg = self.pop_format_context(ast.Str(explanation)) + msg = self.pop_format_context(ast.Constant(explanation)) # Failed if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) gluestr = "\n>assert " else: - assertmsg = ast.Str("") + assertmsg = ast.Constant("") gluestr = "assert " - err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) fmt = self.helper("_format_explanation", err_msg) @@ -909,15 +915,15 @@ class AssertionRewriter(ast.NodeVisitor): hook_call_pass = ast.Expr( self.helper( "_call_assertion_pass", - ast.Num(assert_.lineno), - ast.Str(orig), + ast.Constant(assert_.lineno), + ast.Constant(orig), fmt_pass, ) ) # If any hooks implement assert_pass hook hook_impl_test = ast.If( self.helper("_check_if_assertion_pass_impl"), - self.expl_stmts + [hook_call_pass], + [*self.expl_stmts, hook_call_pass], [], ) statements_pass = [hook_impl_test] @@ -929,7 +935,7 @@ class AssertionRewriter(ast.NodeVisitor): variables = [ ast.Name(name, ast.Store()) for name in self.format_variables ] - clear_format = ast.Assign(variables, ast.NameConstant(None)) + clear_format = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear_format) else: # Original assertion rewriting @@ -940,9 +946,9 @@ class AssertionRewriter(ast.NodeVisitor): assertmsg = self.helper("_format_assertmsg", assert_.msg) explanation = "\n>assert " + explanation else: - assertmsg = ast.Str("") + assertmsg = ast.Constant("") explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) @@ -954,7 +960,7 @@ class AssertionRewriter(ast.NodeVisitor): # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] - clear = ast.Assign(variables, ast.NameConstant(None)) + clear = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear) # Fix locations (line numbers/column offsets). for stmt in self.statements: @@ -962,14 +968,26 @@ class AssertionRewriter(ast.NodeVisitor): ast.copy_location(node, assert_) return self.statements + def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]: + # This method handles the 'walrus operator' repr of the target + # name if it's a local variable or _should_repr_global_name() + # thinks it's acceptable. + locs = ast.Call(self.builtin("locals"), [], []) + target_id = name.target.id + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) + dorepr = self.helper("_should_repr_global_name", name) + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) + return name, self.explanation_param(expr) + def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) return name, self.explanation_param(expr) def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: @@ -986,12 +1004,26 @@ class AssertionRewriter(ast.NodeVisitor): if i: fail_inner: List[ast.stmt] = [] # cond is set in a prior loop iteration below - self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa + self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821 self.expl_stmts = fail_inner + # Check if the left operand is a ast.NamedExpr and the value has already been visited + if ( + isinstance(v, ast.Compare) + and isinstance(v.left, ast.NamedExpr) + and v.left.target.id + in [ + ast_expr.id + for ast_expr in boolop.values[:i] + if hasattr(ast_expr, "id") + ] + ): + pytest_temp = self.variable() + self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment] + v.left.target.id = pytest_temp self.push_format_context() res, expl = self.visit(v) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) - expl_format = self.pop_format_context(ast.Str(expl)) + expl_format = self.pop_format_context(ast.Constant(expl)) call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) if i < levels: @@ -1003,7 +1035,7 @@ class AssertionRewriter(ast.NodeVisitor): self.statements = body = inner self.statements = save self.expl_stmts = fail_save - expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) @@ -1027,10 +1059,18 @@ class AssertionRewriter(ast.NodeVisitor): new_args = [] new_kwargs = [] for arg in call.args: + if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get( + self.scope, {} + ): + arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment] res, expl = self.visit(arg) arg_expls.append(expl) new_args.append(res) for keyword in call.keywords: + if isinstance( + keyword.value, ast.Name + ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}): + keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment] res, expl = self.visit(keyword.value) new_kwargs.append(ast.keyword(keyword.arg, res)) if keyword.arg: @@ -1063,6 +1103,13 @@ class AssertionRewriter(ast.NodeVisitor): def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: self.push_format_context() + # We first check if we have overwritten a variable in the previous assert + if isinstance( + comp.left, ast.Name + ) and comp.left.id in self.variables_overwrite.get(self.scope, {}): + comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment] + if isinstance(comp.left, ast.NamedExpr): + self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment] left_res, left_expl = self.visit(comp.left) if isinstance(comp.left, (ast.Compare, ast.BoolOp)): left_expl = f"({left_expl})" @@ -1074,14 +1121,21 @@ class AssertionRewriter(ast.NodeVisitor): syms = [] results = [left_res] for i, op, next_operand in it: + if ( + isinstance(next_operand, ast.NamedExpr) + and isinstance(left_res, ast.Name) + and next_operand.target.id == left_res.id + ): + next_operand.target.id = self.variable() + self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment] next_res, next_expl = self.visit(next_operand) if isinstance(next_operand, (ast.Compare, ast.BoolOp)): next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] - syms.append(ast.Str(sym)) + syms.append(ast.Constant(sym)) expl = f"{left_expl} {sym} {next_expl}" - expls.append(ast.Str(expl)) + expls.append(ast.Constant(expl)) res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl @@ -1097,6 +1151,7 @@ class AssertionRewriter(ast.NodeVisitor): res: ast.expr = ast.BoolOp(ast.And(), load_names) else: res = load_names[0] + return res, self.explanation_param(self.pop_format_context(expl_call)) @@ -1116,7 +1171,10 @@ def try_makedirs(cache_dir: Path) -> bool: return False except OSError as e: # as of now, EROFS doesn't have an equivalent OSError-subclass - if e.errno == errno.EROFS: + # + # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not + # implemented" for a read-only error + if e.errno in {errno.EROFS, errno.ENOSYS}: return False raise return True @@ -1124,7 +1182,7 @@ def try_makedirs(cache_dir: Path) -> bool: def get_cache_dir(file_path: Path) -> Path: """Return the cache directory to write .pyc files for the given .py file path.""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + if sys.pycache_prefix: # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py index ce148dca095..4fdfd86a519 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/truncate.py @@ -1,12 +1,14 @@ """Utilities for truncating assertion output. Current default behaviour is to truncate assertion explanations at -~8 terminal lines, unless running in "-vv" mode or running on CI. +terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. """ + from typing import List from typing import Optional from _pytest.assertion import util +from _pytest.config import Config from _pytest.nodes import Item @@ -26,7 +28,7 @@ def truncate_if_required( def _should_truncate_item(item: Item) -> bool: """Whether or not this test item is eligible for truncation.""" - verbose = item.config.option.verbose + verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS) return verbose < 2 and not util.running_on_ci() @@ -38,9 +40,9 @@ def _truncate_explanation( """Truncate given list of strings that makes up the assertion explanation. Truncates to either 8 lines, or 640 characters - whichever the input reaches - first. The remaining lines will be replaced by a usage message. + first, taking the truncation explanation into account. The remaining lines + will be replaced by a usage message. """ - if max_lines is None: max_lines = DEFAULT_MAX_LINES if max_chars is None: @@ -48,35 +50,57 @@ def _truncate_explanation( # Check if truncation required input_char_count = len("".join(input_lines)) - if len(input_lines) <= max_lines and input_char_count <= max_chars: + # The length of the truncation explanation depends on the number of lines + # removed but is at least 68 characters: + # The real value is + # 64 (for the base message: + # '...\n...Full output truncated (1 line hidden), use '-vv' to show")' + # ) + # + 1 (for plural) + # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1) + # + 3 for the '...' added to the truncated line + # But if there's more than 100 lines it's very likely that we're going to + # truncate, so we don't need the exact value using log10. + tolerable_max_chars = ( + max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...' + ) + # The truncation explanation add two lines to the output + tolerable_max_lines = max_lines + 2 + if ( + len(input_lines) <= tolerable_max_lines + and input_char_count <= tolerable_max_chars + ): return input_lines - - # Truncate first to max_lines, and then truncate to max_chars if max_chars - # is exceeded. + # Truncate first to max_lines, and then truncate to max_chars if necessary truncated_explanation = input_lines[:max_lines] - truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) - - # Add ellipsis to final line - truncated_explanation[-1] = truncated_explanation[-1] + "..." + truncated_char = True + # We reevaluate the need to truncate chars following removal of some lines + if len("".join(truncated_explanation)) > tolerable_max_chars: + truncated_explanation = _truncate_by_char_count( + truncated_explanation, max_chars + ) + else: + truncated_char = False - # Append useful message to explanation truncated_line_count = len(input_lines) - len(truncated_explanation) - truncated_line_count += 1 # Account for the part-truncated final line - msg = "...Full output truncated" - if truncated_line_count == 1: - msg += f" ({truncated_line_count} line hidden)" + if truncated_explanation[-1]: + # Add ellipsis and take into account part-truncated final line + truncated_explanation[-1] = truncated_explanation[-1] + "..." + if truncated_char: + # It's possible that we did not remove any char from this line + truncated_line_count += 1 else: - msg += f" ({truncated_line_count} lines hidden)" - msg += f", {USAGE_MSG}" - truncated_explanation.extend(["", str(msg)]) - return truncated_explanation + # Add proper ellipsis when we were able to fit a full line exactly + truncated_explanation[-1] = "..." + return [ + *truncated_explanation, + "", + f"...Full output truncated ({truncated_line_count} line" + f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}", + ] def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]: - # Check if truncation required - if len("".join(input_lines)) <= max_chars: - return input_lines - # Find point at which input length exceeds total allowed length iterated_char_count = 0 for iterated_index, input_line in enumerate(input_lines): diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/util.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/util.py index 19f1089c20a..e49c42cfcf7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/util.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/assertion/util.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Utilities for assertion debugging.""" + import collections.abc import os import pprint @@ -7,17 +9,21 @@ from typing import Any from typing import Callable from typing import Iterable from typing import List +from typing import Literal from typing import Mapping from typing import Optional +from typing import Protocol from typing import Sequence +from unicodedata import normalize -import _pytest._code from _pytest import outcomes -from _pytest._io.saferepr import _pformat_dispatch -from _pytest._io.saferepr import safeformat +import _pytest._code +from _pytest._io.pprint import PrettyPrinter from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited from _pytest.config import Config + # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was # loaded and in turn call the hooks defined here as part of the @@ -32,6 +38,11 @@ _assertion_pass: Optional[Callable[[int, str, str], None]] = None _config: Optional[Config] = None +class _HighlightFunc(Protocol): + def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str: + """Apply highlighting to the given source.""" + + def format_explanation(explanation: str) -> str: r"""Format an explanation. @@ -131,51 +142,104 @@ def isiterable(obj: Any) -> bool: try: iter(obj) return not istext(obj) - except TypeError: + except Exception: return False -def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: +def has_default_eq( + obj: object, +) -> bool: + """Check if an instance of an object contains the default eq + + First, we check if the object's __eq__ attribute has __code__, + if so, we check the equally of the method code filename (__code__.co_filename) + to the default one generated by the dataclass and attr module + for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated" + """ + # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68 + if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"): + code_filename = obj.__eq__.__code__.co_filename + + if isattrs(obj): + return "attrs generated eq" in code_filename + + return code_filename == "<string>" # data class + return True + + +def assertrepr_compare( + config, op: str, left: Any, right: Any, use_ascii: bool = False +) -> Optional[List[str]]: """Return specialised explanations for some operators/operands.""" - verbose = config.getoption("verbose") + verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) + + # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier. + # See issue #3246. + use_ascii = ( + isinstance(left, str) + and isinstance(right, str) + and normalize("NFD", left) == normalize("NFD", right) + ) + if verbose > 1: - left_repr = safeformat(left) - right_repr = safeformat(right) + left_repr = saferepr_unlimited(left, use_ascii=use_ascii) + right_repr = saferepr_unlimited(right, use_ascii=use_ascii) else: # XXX: "15 chars indentation" is wrong # ("E AssertionError: assert "); should use term width. maxsize = ( 80 - 15 - len(op) - 2 ) // 2 # 15 chars indentation, 1 space around op - left_repr = saferepr(left, maxsize=maxsize) - right_repr = saferepr(right, maxsize=maxsize) + + left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii) + right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii) summary = f"{left_repr} {op} {right_repr}" + highlighter = config.get_terminal_writer()._highlight explanation = None try: if op == "==": - explanation = _compare_eq_any(left, right, verbose) + explanation = _compare_eq_any(left, right, highlighter, verbose) elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) + elif op == "!=": + if isset(left) and isset(right): + explanation = ["Both sets are equal"] + elif op == ">=": + if isset(left) and isset(right): + explanation = _compare_gte_set(left, right, highlighter, verbose) + elif op == "<=": + if isset(left) and isset(right): + explanation = _compare_lte_set(left, right, highlighter, verbose) + elif op == ">": + if isset(left) and isset(right): + explanation = _compare_gt_set(left, right, highlighter, verbose) + elif op == "<": + if isset(left) and isset(right): + explanation = _compare_lt_set(left, right, highlighter, verbose) + except outcomes.Exit: raise except Exception: + repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash() explanation = [ - "(pytest_assertion plugin: representation of details failed: {}.".format( - _pytest._code.ExceptionInfo.from_current()._getreprcrash() - ), + f"(pytest_assertion plugin: representation of details failed: {repr_crash}.", " Probably an object has a faulty __repr__.)", ] if not explanation: return None - return [summary] + explanation + if explanation[0] != "": + explanation = ["", *explanation] + return [summary, *explanation] -def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: +def _compare_eq_any( + left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0 +) -> List[str]: explanation = [] if istext(left) and istext(right): explanation = _diff_text(left, right, verbose) @@ -188,25 +252,23 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: other_side = right if isinstance(left, ApproxBase) else left explanation = approx_side._repr_compare(other_side) - elif type(left) == type(right) and ( + elif type(left) is type(right) and ( isdatacls(left) or isattrs(left) or isnamedtuple(left) ): # Note: unlike dataclasses/attrs, namedtuples compare only the # field values, not the type or field names. But this branch # intentionally only handles the same-type case, which was often # used in older code bases before dataclasses/attrs were available. - explanation = _compare_eq_cls(left, right, verbose) + explanation = _compare_eq_cls(left, right, highlighter, verbose) elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right, verbose) + explanation = _compare_eq_sequence(left, right, highlighter, verbose) elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right, verbose) + explanation = _compare_eq_set(left, right, highlighter, verbose) elif isdict(left) and isdict(right): - explanation = _compare_eq_dict(left, right, verbose) - elif verbose > 0: - explanation = _compare_eq_verbose(left, right) + explanation = _compare_eq_dict(left, right, highlighter, verbose) if isiterable(left) and isiterable(right): - expl = _compare_eq_iterable(left, right, verbose) + expl = _compare_eq_iterable(left, right, highlighter, verbose) explanation.extend(expl) return explanation @@ -241,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation += [ - "Skipping {} identical trailing " - "characters in diff, use -v to show".format(i) + f"Skipping {i} identical trailing " + "characters in diff, use -v to show" ] left = left[:-i] right = right[:-i] @@ -260,63 +322,40 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: return explanation -def _compare_eq_verbose(left: Any, right: Any) -> List[str]: - keepends = True - left_lines = repr(left).splitlines(keepends) - right_lines = repr(right).splitlines(keepends) - - explanation: List[str] = [] - explanation += ["+" + line for line in left_lines] - explanation += ["-" + line for line in right_lines] - - return explanation - - -def _surrounding_parens_on_own_lines(lines: List[str]) -> None: - """Move opening/closing parenthesis/bracket to own lines.""" - opening = lines[0][:1] - if opening in ["(", "[", "{"]: - lines[0] = " " + lines[0][1:] - lines[:] = [opening] + lines - closing = lines[-1][-1:] - if closing in [")", "]", "}"]: - lines[-1] = lines[-1][:-1] + "," - lines[:] = lines + [closing] - - def _compare_eq_iterable( - left: Iterable[Any], right: Iterable[Any], verbose: int = 0 + left: Iterable[Any], + right: Iterable[Any], + highlighter: _HighlightFunc, + verbose: int = 0, ) -> List[str]: - if not verbose and not running_on_ci(): - return ["Use -v to get the full diff"] + if verbose <= 0 and not running_on_ci(): + return ["Use -v to get more diff"] # dynamic import to speedup pytest import difflib - left_formatting = pprint.pformat(left).splitlines() - right_formatting = pprint.pformat(right).splitlines() - - # Re-format for different output lengths. - lines_left = len(left_formatting) - lines_right = len(right_formatting) - if lines_left != lines_right: - left_formatting = _pformat_dispatch(left).splitlines() - right_formatting = _pformat_dispatch(right).splitlines() - - if lines_left > 1 or lines_right > 1: - _surrounding_parens_on_own_lines(left_formatting) - _surrounding_parens_on_own_lines(right_formatting) + left_formatting = PrettyPrinter().pformat(left).splitlines() + right_formatting = PrettyPrinter().pformat(right).splitlines() - explanation = ["Full diff:"] + explanation = ["", "Full diff:"] # "right" is the expected base against which we compare "left", # see https://github.com/pytest-dev/pytest/issues/3333 explanation.extend( - line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) + highlighter( + "\n".join( + line.rstrip() + for line in difflib.ndiff(right_formatting, left_formatting) + ), + lexer="diff", + ).splitlines() ) return explanation def _compare_eq_sequence( - left: Sequence[Any], right: Sequence[Any], verbose: int = 0 + left: Sequence[Any], + right: Sequence[Any], + highlighter: _HighlightFunc, + verbose: int = 0, ) -> List[str]: comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) explanation: List[str] = [] @@ -339,7 +378,10 @@ def _compare_eq_sequence( left_value = left[i] right_value = right[i] - explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"] + explanation.append( + f"At index {i} diff:" + f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}" + ) break if comparing_bytes: @@ -359,34 +401,91 @@ def _compare_eq_sequence( extra = saferepr(right[len_left]) if len_diff == 1: - explanation += [f"{dir_with_more} contains one more item: {extra}"] + explanation += [ + f"{dir_with_more} contains one more item: {highlighter(extra)}" + ] else: explanation += [ "%s contains %d more items, first extra item: %s" - % (dir_with_more, len_diff, extra) + % (dir_with_more, len_diff, highlighter(extra)) ] return explanation def _compare_eq_set( - left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, ) -> List[str]: explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append("Extra items in the left set:") - for item in diff_left: - explanation.append(saferepr(item)) - if diff_right: - explanation.append("Extra items in the right set:") - for item in diff_right: - explanation.append(saferepr(item)) + explanation.extend(_set_one_sided_diff("left", left, right, highlighter)) + explanation.extend(_set_one_sided_diff("right", right, left, highlighter)) + return explanation + + +def _compare_gt_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> List[str]: + explanation = _compare_gte_set(left, right, highlighter) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_lt_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> List[str]: + explanation = _compare_lte_set(left, right, highlighter) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_gte_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> List[str]: + return _set_one_sided_diff("right", right, left, highlighter) + + +def _compare_lte_set( + left: AbstractSet[Any], + right: AbstractSet[Any], + highlighter: _HighlightFunc, + verbose: int = 0, +) -> List[str]: + return _set_one_sided_diff("left", left, right, highlighter) + + +def _set_one_sided_diff( + posn: str, + set1: AbstractSet[Any], + set2: AbstractSet[Any], + highlighter: _HighlightFunc, +) -> List[str]: + explanation = [] + diff = set1 - set2 + if diff: + explanation.append(f"Extra items in the {posn} set:") + for item in diff: + explanation.append(highlighter(saferepr(item))) return explanation def _compare_eq_dict( - left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 + left: Mapping[Any, Any], + right: Mapping[Any, Any], + highlighter: _HighlightFunc, + verbose: int = 0, ) -> List[str]: explanation: List[str] = [] set_left = set(left) @@ -397,12 +496,16 @@ def _compare_eq_dict( explanation += ["Omitting %s identical items, use -vv to show" % len(same)] elif same: explanation += ["Common items:"] - explanation += pprint.pformat(same).splitlines() + explanation += highlighter(pprint.pformat(same)).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: explanation += ["Differing items:"] for k in diff: - explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] + explanation += [ + highlighter(saferepr({k: left[k]})) + + " != " + + highlighter(saferepr({k: right[k]})) + ] extra_left = set_left - set_right len_extra_left = len(extra_left) if len_extra_left: @@ -411,7 +514,7 @@ def _compare_eq_dict( % (len_extra_left, "" if len_extra_left == 1 else "s") ) explanation.extend( - pprint.pformat({k: left[k] for k in extra_left}).splitlines() + highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines() ) extra_right = set_right - set_left len_extra_right = len(extra_right) @@ -421,15 +524,21 @@ def _compare_eq_dict( % (len_extra_right, "" if len_extra_right == 1 else "s") ) explanation.extend( - pprint.pformat({k: right[k] for k in extra_right}).splitlines() + highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines() ) return explanation -def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: +def _compare_eq_cls( + left: Any, right: Any, highlighter: _HighlightFunc, verbose: int +) -> List[str]: + if not has_default_eq(left): + return [] if isdatacls(left): - all_fields = left.__dataclass_fields__ - fields_to_check = [field for field, info in all_fields.items() if info.compare] + import dataclasses + + all_fields = dataclasses.fields(left) + fields_to_check = [info.name for info in all_fields if info.compare] elif isattrs(left): all_fields = left.__attrs_attrs__ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")] @@ -454,21 +563,23 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: explanation.append("Omitting %s identical items, use -vv to show" % len(same)) elif same: explanation += ["Matching attributes:"] - explanation += pprint.pformat(same).splitlines() + explanation += highlighter(pprint.pformat(same)).splitlines() if diff: explanation += ["Differing attributes:"] - explanation += pprint.pformat(diff).splitlines() + explanation += highlighter(pprint.pformat(diff)).splitlines() for field in diff: field_left = getattr(left, field) field_right = getattr(right, field) explanation += [ "", - "Drill down into differing attribute %s:" % field, - ("%s%s: %r != %r") % (indent, field, field_left, field_right), + f"Drill down into differing attribute {field}:", + f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}", ] explanation += [ indent + line - for line in _compare_eq_any(field_left, field_right, verbose) + for line in _compare_eq_any( + field_left, field_right, highlighter, verbose + ) ] return explanation diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py index 681d02b4093..a9cbb77cbbb 100644..100755 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/cacheprovider.py @@ -1,10 +1,15 @@ +# mypy: allow-untyped-defs """Implementation of the cache provider.""" + # This plugin was not named "cache" to avoid conflicts with the external # pytest-cache version. +import dataclasses import json import os from pathlib import Path +import tempfile from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import List @@ -12,14 +17,11 @@ from typing import Optional from typing import Set from typing import Union -import attr - from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -28,8 +30,8 @@ from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session -from _pytest.python import Module -from _pytest.python import Package +from _pytest.nodes import Directory +from _pytest.nodes import File from _pytest.reports import TestReport @@ -53,10 +55,12 @@ Signature: 8a477f597d28d172789f06886806bc55 @final -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class Cache: - _cachedir: Path = attr.ib(repr=False) - _config: Config = attr.ib(repr=False) + """Instance of the `cache` fixture.""" + + _cachedir: Path = dataclasses.field(repr=False) + _config: Config = dataclasses.field(repr=False) # Sub-directory under cache-dir for directories created by `mkdir()`. _CACHE_PREFIX_DIRS = "d" @@ -111,6 +115,7 @@ class Cache: """ check_ispytest(_ispytest) import warnings + from _pytest.warning_types import PytestCacheWarning warnings.warn( @@ -119,6 +124,10 @@ class Cache: stacklevel=3, ) + def _mkdir(self, path: Path) -> None: + self._ensure_cache_dir_and_supporting_files() + path.mkdir(exist_ok=True, parents=True) + def mkdir(self, name: str) -> Path: """Return a directory path object with the given name. @@ -137,7 +146,7 @@ class Cache: if len(path.parts) > 1: raise ValueError("name is not allowed to contain path separators") res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) - res.mkdir(exist_ok=True, parents=True) + self._mkdir(res) return res def _getvaluepath(self, key: str) -> Path: @@ -157,7 +166,7 @@ class Cache: """ path = self._getvaluepath(key) try: - with path.open("r") as f: + with path.open("r", encoding="UTF-8") as f: return json.load(f) except (ValueError, OSError): return default @@ -174,36 +183,58 @@ class Cache: """ path = self._getvaluepath(key) try: - if path.parent.is_dir(): - cache_dir_exists_already = True - else: - cache_dir_exists_already = self._cachedir.exists() - path.parent.mkdir(exist_ok=True, parents=True) - except OSError: - self.warn("could not create cache path {path}", path=path, _ispytest=True) + self._mkdir(path.parent) + except OSError as exc: + self.warn( + f"could not create cache path {path}: {exc}", + _ispytest=True, + ) return - if not cache_dir_exists_already: - self._ensure_supporting_files() - data = json.dumps(value, indent=2) + data = json.dumps(value, ensure_ascii=False, indent=2) try: - f = path.open("w") - except OSError: - self.warn("cache could not write path {path}", path=path, _ispytest=True) + f = path.open("w", encoding="UTF-8") + except OSError as exc: + self.warn( + f"cache could not write path {path}: {exc}", + _ispytest=True, + ) else: with f: f.write(data) - def _ensure_supporting_files(self) -> None: - """Create supporting files in the cache dir that are not really part of the cache.""" - readme_path = self._cachedir / "README.md" - readme_path.write_text(README_CONTENT) - - gitignore_path = self._cachedir.joinpath(".gitignore") - msg = "# Created by pytest automatically.\n*\n" - gitignore_path.write_text(msg, encoding="UTF-8") + def _ensure_cache_dir_and_supporting_files(self) -> None: + """Create the cache dir and its supporting files.""" + if self._cachedir.is_dir(): + return - cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") - cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) + self._cachedir.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory( + prefix="pytest-cache-files-", + dir=self._cachedir.parent, + ) as newpath: + path = Path(newpath) + + # Reset permissions to the default, see #12308. + # Note: there's no way to get the current umask atomically, eek. + umask = os.umask(0o022) + os.umask(umask) + path.chmod(0o777 - umask) + + with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f: + f.write(README_CONTENT) + with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f: + f.write("# Created by pytest automatically.\n*\n") + with open(path.joinpath("CACHEDIR.TAG"), "xb") as f: + f.write(CACHEDIR_TAG_CONTENT) + + path.rename(self._cachedir) + # Create a directory in place of the one we just moved so that `TemporaryDirectory`'s + # cleanup doesn't complain. + # + # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See + # https://github.com/python/cpython/issues/74168. Note that passing delete=False would + # do the wrong thing in case of errors and isn't supported until python 3.12. + path.mkdir() class LFPluginCollWrapper: @@ -211,34 +242,34 @@ class LFPluginCollWrapper: self.lfplugin = lfplugin self._collected_at_least_one_failure = False - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: nodes.Collector): - if isinstance(collector, Session): - out = yield - res: CollectReport = out.get_result() - + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + res = yield + if isinstance(collector, (Session, Directory)): # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths + # Use stable sort to prioritize last failed. + def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool: + return node.path in lf_paths + res.result = sorted( res.result, - # use stable sort to priorize last failed - key=lambda x: x.path in lf_paths, + key=sort_key, reverse=True, ) - return - elif isinstance(collector, Module): + elif isinstance(collector, File): if collector.path in self.lfplugin._last_failed_paths: - out = yield - res = out.get_result() result = res.result lastfailed = self.lfplugin.lastfailed # Only filter with known failures. if not self._collected_at_least_one_failure: if not any(x.nodeid in lastfailed for x in result): - return + return res self.lfplugin.config.pluginmanager.register( LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" ) @@ -254,8 +285,8 @@ class LFPluginCollWrapper: # Keep all sub-collectors. or isinstance(x, nodes.Collector) ] - return - yield + + return res class LFPluginCollSkipfiles: @@ -266,10 +297,7 @@ class LFPluginCollSkipfiles: def pytest_make_collect_report( self, collector: nodes.Collector ) -> Optional[CollectReport]: - # Packages are Modules, but _last_failed_paths only contains - # test-bearing paths and doesn't try to include the paths of their - # packages, so don't filter them. - if isinstance(collector, Module) and not isinstance(collector, Package): + if isinstance(collector, File): if collector.path not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 @@ -299,9 +327,14 @@ class LFPlugin: ) def get_last_failed_paths(self) -> Set[Path]: - """Return a set with all Paths()s of the previously failed nodeids.""" + """Return a set with all Paths of the previously failed nodeids and + their parents.""" rootpath = self.config.rootpath - result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} + result = set() + for nodeid in self.lastfailed: + path = rootpath / nodeid.split("::")[0] + result.add(path) + result.update(path.parents) return {x for x in result if x.exists()} def pytest_report_collectionfinish(self) -> Optional[str]: @@ -324,14 +357,14 @@ class LFPlugin: else: self.lastfailed[report.nodeid] = True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if not self.active: - return + return res if self.lastfailed: previously_failed = [] @@ -358,15 +391,13 @@ class LFPlugin: noun = "failure" if self._previously_failed_count == 1 else "failures" suffix = " first" if self.config.getoption("failedfirst") else "" - self._report_status = "rerun previous {count} {noun}{suffix}".format( - count=self._previously_failed_count, suffix=suffix, noun=noun + self._report_status = ( + f"rerun previous {self._previously_failed_count} {noun}{suffix}" ) if self._skipped_files > 0: files_noun = "file" if self._skipped_files == 1 else "files" - self._report_status += " (skipped {files} {files_noun})".format( - files=self._skipped_files, files_noun=files_noun - ) + self._report_status += f" (skipped {self._skipped_files} {files_noun})" else: self._report_status = "no previously failed tests, " if self.config.getoption("last_failed_no_failures") == "none": @@ -376,6 +407,8 @@ class LFPlugin: else: self._report_status += "not deselecting items." + return res + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "workerinput"): @@ -396,11 +429,11 @@ class NFPlugin: assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if self.active: new_items: Dict[str, nodes.Item] = {} @@ -418,8 +451,10 @@ class NFPlugin: else: self.cached_nodeids.update(item.nodeid for item in items) + return res + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: - return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] + return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) def pytest_sessionfinish(self) -> None: config = self.config @@ -440,7 +475,7 @@ def pytest_addoption(parser: Parser) -> None: "--last-failed", action="store_true", dest="lf", - help="rerun only the tests that failed " + help="Rerun only the tests that failed " "at the last run (or all if none failed)", ) group.addoption( @@ -448,7 +483,7 @@ def pytest_addoption(parser: Parser) -> None: "--failed-first", action="store_true", dest="failedfirst", - help="run all tests, but run the last failures first.\n" + help="Run all tests, but run the last failures first. " "This may re-order tests and thus lead to " "repeated fixture setup/teardown.", ) @@ -457,7 +492,7 @@ def pytest_addoption(parser: Parser) -> None: "--new-first", action="store_true", dest="newfirst", - help="run tests from new files first, then the rest of the tests " + help="Run tests from new files first, then the rest of the tests " "sorted by file mtime", ) group.addoption( @@ -466,7 +501,7 @@ def pytest_addoption(parser: Parser) -> None: nargs="?", dest="cacheshow", help=( - "show cache contents, don't perform collection or tests. " + "Show cache contents, don't perform collection or tests. " "Optional argument: glob (default: '*')." ), ) @@ -474,12 +509,12 @@ def pytest_addoption(parser: Parser) -> None: "--cache-clear", action="store_true", dest="cacheclear", - help="remove all cache contents at start of test run.", + help="Remove all cache contents at start of test run", ) cache_dir_default = ".pytest_cache" if "TOX_ENV_DIR" in os.environ: cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default) - parser.addini("cache_dir", default=cache_dir_default, help="cache directory path.") + parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path") group.addoption( "--lfnf", "--last-failed-no-failures", @@ -487,12 +522,16 @@ def pytest_addoption(parser: Parser) -> None: dest="last_failed_no_failures", choices=("all", "none"), default="all", - help="which tests to run with no previously (known) failures.", + help="With ``--lf``, determines whether to execute tests when there " + "are no previously (known) failures or when no " + "cached ``lastfailed`` data was found. " + "``all`` (the default) runs the full test suite again. " + "``none`` just emits a message about no known failures and exits successfully.", ) def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.cacheshow: + if config.option.cacheshow and not config.option.help: from _pytest.main import wrap_session return wrap_session(config, cacheshow) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/capture.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/capture.py index 884f035e299..3f6a2510348 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/capture.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/capture.py @@ -1,23 +1,34 @@ +# mypy: allow-untyped-defs """Per-test stdout/stderr capturing mechanism.""" + +import abc +import collections import contextlib -import functools import io +from io import UnsupportedOperation import os import sys -from io import UnsupportedOperation from tempfile import TemporaryFile +from types import TracebackType from typing import Any from typing import AnyStr +from typing import BinaryIO +from typing import Final +from typing import final from typing import Generator from typing import Generic +from typing import Iterable from typing import Iterator +from typing import List +from typing import Literal +from typing import NamedTuple from typing import Optional from typing import TextIO from typing import Tuple +from typing import Type from typing import TYPE_CHECKING from typing import Union -from _pytest.compat import final from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -27,11 +38,10 @@ from _pytest.fixtures import SubRequest from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item +from _pytest.reports import CollectReport -if TYPE_CHECKING: - from typing_extensions import Literal - _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] +_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] def pytest_addoption(parser: Parser) -> None: @@ -42,14 +52,14 @@ def pytest_addoption(parser: Parser) -> None: default="fd", metavar="method", choices=["fd", "sys", "no", "tee-sys"], - help="per-test capturing method: one of fd|sys|no|tee-sys.", + help="Per-test capturing method: one of fd|sys|no|tee-sys", ) group._addoption( "-s", action="store_const", const="no", dest="capture", - help="shortcut for --capture=no.", + help="Shortcut for --capture=no", ) @@ -68,8 +78,8 @@ def _colorama_workaround() -> None: pass -def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: - """Workaround for Windows Unicode console handling on Python>=3.6. +def _windowsconsoleio_workaround(stream: TextIO) -> None: + """Workaround for Windows Unicode console handling. Python 3.6 implemented Unicode console handling for Windows. This works by reading/writing to the raw console handle using @@ -96,23 +106,22 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: return # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666). - if not hasattr(stream, "buffer"): # type: ignore[unreachable] + if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore] return - buffered = hasattr(stream.buffer, "raw") - raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined] + raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer - if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined] + if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore] return def _reopen_stdio(f, mode): - if not buffered and mode[0] == "w": + if not hasattr(stream.buffer, "raw") and mode[0] == "w": buffering = 0 else: buffering = -1 return io.TextIOWrapper( - open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type] + open(os.dup(f.fileno()), mode, buffering), f.encoding, f.errors, f.newlines, @@ -124,11 +133,11 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: sys.stderr = _reopen_stdio(sys.stderr, "wb") -@hookimpl(hookwrapper=True) -def pytest_load_initial_conftests(early_config: Config): +@hookimpl(wrapper=True) +def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]: ns = early_config.known_args_namespace if ns.capture == "fd": - _py36_windowsconsoleio_workaround(sys.stdout) + _windowsconsoleio_workaround(sys.stdout) _colorama_workaround() pluginmanager = early_config.pluginmanager capman = CaptureManager(ns.capture) @@ -139,12 +148,16 @@ def pytest_load_initial_conftests(early_config: Config): # Finally trigger conftest loading but while capturing (issue #93). capman.start_global_capturing() - outcome = yield - capman.suspend_global_capture() - if outcome.excinfo is not None: + try: + try: + yield + finally: + capman.suspend_global_capture() + except BaseException: out, err = capman.read_global_capture() sys.stdout.write(out) sys.stderr.write(err) + raise # IO Helpers. @@ -185,53 +198,151 @@ class TeeCaptureIO(CaptureIO): return self._other.write(s) -class DontReadFromInput: - encoding = None +class DontReadFromInput(TextIO): + @property + def encoding(self) -> str: + return sys.__stdin__.encoding - def read(self, *args): + def read(self, size: int = -1) -> str: raise OSError( "pytest: reading from stdin while output is captured! Consider using `-s`." ) readline = read - readlines = read - __next__ = read - def __iter__(self): + def __next__(self) -> str: + return self.readline() + + def readlines(self, hint: Optional[int] = -1) -> List[str]: + raise OSError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) + + def __iter__(self) -> Iterator[str]: return self def fileno(self) -> int: raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + def flush(self) -> None: + raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()") + def isatty(self) -> bool: return False def close(self) -> None: pass - @property - def buffer(self): + def readable(self) -> bool: + return False + + def seek(self, offset: int, whence: int = 0) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)") + + def seekable(self) -> bool: + return False + + def tell(self) -> int: + raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") + + def truncate(self, size: Optional[int] = None) -> int: + raise UnsupportedOperation("cannot truncate stdin") + + def write(self, data: str) -> int: + raise UnsupportedOperation("cannot write to stdin") + + def writelines(self, lines: Iterable[str]) -> None: + raise UnsupportedOperation("Cannot write to stdin") + + def writable(self) -> bool: + return False + + def __enter__(self) -> "DontReadFromInput": return self + def __exit__( + self, + type: Optional[Type[BaseException]], + value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + pass + + @property + def buffer(self) -> BinaryIO: + # The str/bytes doesn't actually matter in this type, so OK to fake. + return self # type: ignore[return-value] + # Capture classes. +class CaptureBase(abc.ABC, Generic[AnyStr]): + EMPTY_BUFFER: AnyStr + + @abc.abstractmethod + def __init__(self, fd: int) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def start(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def done(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def suspend(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def resume(self) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def writeorg(self, data: AnyStr) -> None: + raise NotImplementedError() + + @abc.abstractmethod + def snap(self) -> AnyStr: + raise NotImplementedError() + + patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} -class NoCapture: - EMPTY_BUFFER = None - __init__ = start = done = suspend = resume = lambda *args: None +class NoCapture(CaptureBase[str]): + EMPTY_BUFFER = "" + + def __init__(self, fd: int) -> None: + pass + + def start(self) -> None: + pass + def done(self) -> None: + pass -class SysCaptureBinary: + def suspend(self) -> None: + pass - EMPTY_BUFFER = b"" + def resume(self) -> None: + pass - def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None: + def snap(self) -> str: + return "" + + def writeorg(self, data: str) -> None: + pass + + +class SysCaptureBase(CaptureBase[AnyStr]): + def __init__( + self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False + ) -> None: name = patchsysdict[fd] - self._old = getattr(sys, name) + self._old: TextIO = getattr(sys, name) self.name = name if tmpfile is None: if name == "stdin": @@ -271,14 +382,6 @@ class SysCaptureBinary: setattr(sys, self.name, self.tmpfile) self._state = "started" - def snap(self): - self._assert_state("snap", ("started", "suspended")) - self.tmpfile.seek(0) - res = self.tmpfile.buffer.read() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - def done(self) -> None: self._assert_state("done", ("initialized", "started", "suspended", "done")) if self._state == "done": @@ -300,36 +403,43 @@ class SysCaptureBinary: setattr(sys, self.name, self.tmpfile) self._state = "started" - def writeorg(self, data) -> None: + +class SysCaptureBinary(SysCaptureBase[bytes]): + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: bytes) -> None: self._assert_state("writeorg", ("started", "suspended")) self._old.flush() self._old.buffer.write(data) self._old.buffer.flush() -class SysCapture(SysCaptureBinary): - EMPTY_BUFFER = "" # type: ignore[assignment] +class SysCapture(SysCaptureBase[str]): + EMPTY_BUFFER = "" - def snap(self): + def snap(self) -> str: + self._assert_state("snap", ("started", "suspended")) + assert isinstance(self.tmpfile, CaptureIO) res = self.tmpfile.getvalue() self.tmpfile.seek(0) self.tmpfile.truncate() return res - def writeorg(self, data): + def writeorg(self, data: str) -> None: self._assert_state("writeorg", ("started", "suspended")) self._old.write(data) self._old.flush() -class FDCaptureBinary: - """Capture IO to/from a given OS-level file descriptor. - - snap() produces `bytes`. - """ - - EMPTY_BUFFER = b"" - +class FDCaptureBase(CaptureBase[AnyStr]): def __init__(self, targetfd: int) -> None: self.targetfd = targetfd @@ -354,8 +464,8 @@ class FDCaptureBinary: self.targetfd_save = os.dup(targetfd) if targetfd == 0: - self.tmpfile = open(os.devnull) - self.syscapture = SysCapture(targetfd) + self.tmpfile = open(os.devnull, encoding="utf-8") + self.syscapture: CaptureBase[str] = SysCapture(targetfd) else: self.tmpfile = EncodedFile( TemporaryFile(buffering=0), @@ -367,17 +477,14 @@ class FDCaptureBinary: if targetfd in patchsysdict: self.syscapture = SysCapture(targetfd, self.tmpfile) else: - self.syscapture = NoCapture() + self.syscapture = NoCapture(targetfd) self._state = "initialized" def __repr__(self) -> str: - return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format( - self.__class__.__name__, - self.targetfd, - self.targetfd_save, - self._state, - self.tmpfile, + return ( + f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} " + f"_state={self._state!r} tmpfile={self.tmpfile!r}>" ) def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: @@ -394,14 +501,6 @@ class FDCaptureBinary: self.syscapture.start() self._state = "started" - def snap(self): - self._assert_state("snap", ("started", "suspended")) - self.tmpfile.seek(0) - res = self.tmpfile.buffer.read() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - def done(self) -> None: """Stop capturing, restore streams, return original capture file, seeked to position zero.""" @@ -434,22 +533,38 @@ class FDCaptureBinary: os.dup2(self.tmpfile.fileno(), self.targetfd) self._state = "started" - def writeorg(self, data): + +class FDCaptureBinary(FDCaptureBase[bytes]): + """Capture IO to/from a given OS-level file descriptor. + + snap() produces `bytes`. + """ + + EMPTY_BUFFER = b"" + + def snap(self) -> bytes: + self._assert_state("snap", ("started", "suspended")) + self.tmpfile.seek(0) + res = self.tmpfile.buffer.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def writeorg(self, data: bytes) -> None: """Write to original file descriptor.""" self._assert_state("writeorg", ("started", "suspended")) os.write(self.targetfd_save, data) -class FDCapture(FDCaptureBinary): +class FDCapture(FDCaptureBase[str]): """Capture IO to/from a given OS-level file descriptor. snap() produces text. """ - # Ignore type because it doesn't match the type in the superclass (bytes). - EMPTY_BUFFER = "" # type: ignore + EMPTY_BUFFER = "" - def snap(self): + def snap(self) -> str: self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) res = self.tmpfile.read() @@ -457,85 +572,55 @@ class FDCapture(FDCaptureBinary): self.tmpfile.truncate() return res - def writeorg(self, data): + def writeorg(self, data: str) -> None: """Write to original file descriptor.""" - super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream + self._assert_state("writeorg", ("started", "suspended")) + # XXX use encoding of original stream + os.write(self.targetfd_save, data.encode("utf-8")) # MultiCapture -# This class was a namedtuple, but due to mypy limitation[0] it could not be -# made generic, so was replaced by a regular class which tries to emulate the -# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can -# make it a namedtuple again. -# [0]: https://github.com/python/mypy/issues/685 -@final -@functools.total_ordering -class CaptureResult(Generic[AnyStr]): - """The result of :method:`CaptureFixture.readouterr`.""" - - __slots__ = ("out", "err") - - def __init__(self, out: AnyStr, err: AnyStr) -> None: - self.out: AnyStr = out - self.err: AnyStr = err - - def __len__(self) -> int: - return 2 - - def __iter__(self) -> Iterator[AnyStr]: - return iter((self.out, self.err)) - - def __getitem__(self, item: int) -> AnyStr: - return tuple(self)[item] - - def _replace( - self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None - ) -> "CaptureResult[AnyStr]": - return CaptureResult( - out=self.out if out is None else out, err=self.err if err is None else err - ) - - def count(self, value: AnyStr) -> int: - return tuple(self).count(value) +# Generic NamedTuple only supported since Python 3.11. +if sys.version_info >= (3, 11) or TYPE_CHECKING: - def index(self, value) -> int: - return tuple(self).index(value) + @final + class CaptureResult(NamedTuple, Generic[AnyStr]): + """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`.""" - def __eq__(self, other: object) -> bool: - if not isinstance(other, (CaptureResult, tuple)): - return NotImplemented - return tuple(self) == tuple(other) + out: AnyStr + err: AnyStr - def __hash__(self) -> int: - return hash(tuple(self)) +else: - def __lt__(self, other: object) -> bool: - if not isinstance(other, (CaptureResult, tuple)): - return NotImplemented - return tuple(self) < tuple(other) + class CaptureResult( + collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024 + Generic[AnyStr], + ): + """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`.""" - def __repr__(self) -> str: - return f"CaptureResult(out={self.out!r}, err={self.err!r})" + __slots__ = () class MultiCapture(Generic[AnyStr]): _state = None _in_suspended = False - def __init__(self, in_, out, err) -> None: - self.in_ = in_ - self.out = out - self.err = err + def __init__( + self, + in_: Optional[CaptureBase[AnyStr]], + out: Optional[CaptureBase[AnyStr]], + err: Optional[CaptureBase[AnyStr]], + ) -> None: + self.in_: Optional[CaptureBase[AnyStr]] = in_ + self.out: Optional[CaptureBase[AnyStr]] = out + self.err: Optional[CaptureBase[AnyStr]] = err def __repr__(self) -> str: - return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( - self.out, - self.err, - self.in_, - self._state, - self._in_suspended, + return ( + f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} " + f"_state={self._state!r} _in_suspended={self._in_suspended!r}>" ) def start_capturing(self) -> None: @@ -551,8 +636,10 @@ class MultiCapture(Generic[AnyStr]): """Pop current snapshot out/err capture and flush to orig streams.""" out, err = self.readouterr() if out: + assert self.out is not None self.out.writeorg(out) if err: + assert self.err is not None self.err.writeorg(err) return out, err @@ -573,6 +660,7 @@ class MultiCapture(Generic[AnyStr]): if self.err: self.err.resume() if self._in_suspended: + assert self.in_ is not None self.in_.resume() self._in_suspended = False @@ -595,10 +683,11 @@ class MultiCapture(Generic[AnyStr]): def readouterr(self) -> CaptureResult[AnyStr]: out = self.out.snap() if self.out else "" err = self.err.snap() if self.err else "" - return CaptureResult(out, err) + # TODO: This type error is real, need to fix. + return CaptureResult(out, err) # type: ignore[arg-type] -def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: +def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": @@ -634,14 +723,15 @@ class CaptureManager: needed to ensure the fixtures take precedence over the global capture. """ - def __init__(self, method: "_CaptureMethod") -> None: - self._method = method + def __init__(self, method: _CaptureMethod) -> None: + self._method: Final = method self._global_capturing: Optional[MultiCapture[str]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None def __repr__(self) -> str: - return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format( - self._method, self._global_capturing, self._capture_fixture + return ( + f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} " + f"_capture_fixture={self._capture_fixture!r}>" ) def is_capturing(self) -> Union[str, bool]: @@ -697,9 +787,7 @@ class CaptureManager: current_fixture = self._capture_fixture.request.fixturename requested_fixture = capture_fixture.request.fixturename capture_fixture.request.raiseerror( - "cannot use {} and {} at the same time".format( - requested_fixture, current_fixture - ) + f"cannot use {requested_fixture} and {current_fixture} at the same time" ) self._capture_fixture = capture_fixture @@ -754,41 +842,45 @@ class CaptureManager: self.deactivate_fixture() self.suspend_global_capture(in_=False) - out, err = self.read_global_capture() - item.add_report_section(when, "stdout", out) - item.add_report_section(when, "stderr", err) + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) # Hooks - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: Collector): + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: Collector + ) -> Generator[None, CollectReport, CollectReport]: if isinstance(collector, File): self.resume_global_capture() - outcome = yield - self.suspend_global_capture() + try: + rep = yield + finally: + self.suspend_global_capture() out, err = self.read_global_capture() - rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) if err: rep.sections.append(("Captured stderr", err)) else: - yield + rep = yield + return rep - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): - yield + return (yield) @hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self) -> None: @@ -804,14 +896,18 @@ class CaptureFixture(Generic[AnyStr]): :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" def __init__( - self, captureclass, request: SubRequest, *, _ispytest: bool = False + self, + captureclass: Type[CaptureBase[AnyStr]], + request: SubRequest, + *, + _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - self.captureclass = captureclass + self.captureclass: Type[CaptureBase[AnyStr]] = captureclass self.request = request self._capture: Optional[MultiCapture[AnyStr]] = None - self._captured_out = self.captureclass.EMPTY_BUFFER - self._captured_err = self.captureclass.EMPTY_BUFFER + self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER + self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER def _start(self) -> None: if self._capture is None: @@ -866,7 +962,9 @@ class CaptureFixture(Generic[AnyStr]): @contextlib.contextmanager def disabled(self) -> Generator[None, None, None]: """Temporarily disable capturing while inside the ``with`` block.""" - capmanager = self.request.config.pluginmanager.getplugin("capturemanager") + capmanager: CaptureManager = self.request.config.pluginmanager.getplugin( + "capturemanager" + ) with capmanager.global_and_fixture_disabled(): yield @@ -876,14 +974,24 @@ class CaptureFixture(Generic[AnyStr]): @fixture def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: - """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -893,14 +1001,24 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: @fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: - """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -910,14 +1028,24 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, @fixture def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: - """Enable text capturing of writes to file descriptors ``1`` and ``2``. + r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture @@ -927,14 +1055,25 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: @fixture def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: - """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. + + Example: + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + """ - capman = request.config.pluginmanager.getplugin("capturemanager") - capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True) capman.set_fixture(capture_fixture) capture_fixture._start() yield capture_fixture diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/compat.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/compat.py index 7703dee8c5a..614848e0dba 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/compat.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/compat.py @@ -1,32 +1,24 @@ +# mypy: allow-untyped-defs """Python version compatibility code.""" + +from __future__ import annotations + +import dataclasses import enum import functools import inspect -import os -import sys -from contextlib import contextmanager from inspect import Parameter from inspect import signature +import os from pathlib import Path +import sys from typing import Any from typing import Callable -from typing import Generic -from typing import Optional -from typing import Tuple -from typing import TYPE_CHECKING -from typing import TypeVar -from typing import Union - -import attr -import py - -if TYPE_CHECKING: - from typing import NoReturn - from typing_extensions import Final +from typing import Final +from typing import NoReturn +import py -_T = TypeVar("_T") -_S = TypeVar("_S") #: constant to prepare valuing pylib path replacements/lazy proxies later on # intended for removal in pytest 8.0 or 9.0 @@ -37,7 +29,7 @@ LEGACY_PATH = py.path. local # fmt: on -def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: +def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH: """Internal wrapper to prepare lazy proxies for legacy_path instances""" return LEGACY_PATH(path) @@ -47,18 +39,9 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions class NotSetType(enum.Enum): token = 0 -NOTSET: "Final" = NotSetType.token # noqa: E305 +NOTSET: Final = NotSetType.token # fmt: on -if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata -else: - import importlib_metadata # noqa: F401 - - -def _format_args(func: Callable[..., Any]) -> str: - return str(signature(func)) - def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) @@ -70,7 +53,7 @@ def iscoroutinefunction(func: object) -> bool: def syntax, and doesn't contain yield), or a function decorated with @asyncio.coroutine. - Note: copied and modified from Python 3.5's builtin couroutines.py to avoid + Note: copied and modified from Python 3.5's builtin coroutines.py to avoid importing asyncio directly, which in turns also initializes the "logging" module as a side-effect (see issue #8). """ @@ -83,7 +66,7 @@ def is_async_function(func: object) -> bool: return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) -def getlocation(function, curdir: Optional[str] = None) -> str: +def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -117,12 +100,11 @@ def num_mock_patch_args(function) -> int: def getfuncargnames( - function: Callable[..., Any], + function: Callable[..., object], *, name: str = "", - is_method: bool = False, - cls: Optional[type] = None, -) -> Tuple[str, ...]: + cls: type | None = None, +) -> tuple[str, ...]: """Return the names of a function's mandatory arguments. Should return the names of all function arguments that: @@ -131,9 +113,8 @@ def getfuncargnames( * Aren't bound with functools.partial. * Aren't replaced with mocks. - The is_method and cls arguments indicate that the function should - be treated as a bound method even though it's not unless, only in - the case of cls, the function is a static method. + The cls arguments indicate that the function should be treated as a bound + method even though it's not unless the function is a static method. The name parameter should be the original name in which the function was collected. """ @@ -171,7 +152,7 @@ def getfuncargnames( # If this function should be treated as a bound method even though # it's passed as an unbound method or function, remove the first # parameter name. - if is_method or ( + if ( # Not using `getattr` because we don't want to resolve the staticmethod. # Not using `cls.__dict__` because we want to check the entire MRO. cls @@ -186,18 +167,7 @@ def getfuncargnames( return arg_names -if sys.version_info < (3, 7): - - @contextmanager - def nullcontext(): - yield - - -else: - from contextlib import nullcontext as nullcontext # noqa: F401 - - -def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: +def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]: # Note: this code intentionally mirrors the code at the beginning of # getfuncargnames, to get the arguments which were excluded from its result # because they had default values. @@ -217,25 +187,13 @@ _non_printable_ascii_translate_table.update( ) -def _translate_non_printable(s: str) -> str: - return s.translate(_non_printable_ascii_translate_table) - - -STRING_TYPES = bytes, str - - -def _bytes_to_ascii(val: bytes) -> str: - return val.decode("ascii", "backslashreplace") - - -def ascii_escaped(val: Union[bytes, str]) -> str: +def ascii_escaped(val: bytes | str) -> str: r"""If val is pure ASCII, return it as an str, otherwise, escape bytes objects into a sequence of escaped bytes: b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' - and escapes unicode objects into a sequence of escaped unicode - ids, e.g.: + and escapes strings into a sequence of escaped unicode ids, e.g.: r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' @@ -246,13 +204,13 @@ def ascii_escaped(val: Union[bytes, str]) -> str: a UTF-8 string. """ if isinstance(val, bytes): - ret = _bytes_to_ascii(val) + ret = val.decode("ascii", "backslashreplace") else: ret = val.encode("unicode_escape").decode("ascii") - return _translate_non_printable(ret) + return ret.translate(_non_printable_ascii_translate_table) -@attr.s +@dataclasses.dataclass class _PytestWrapper: """Dummy wrapper around a function object for internal use only. @@ -261,7 +219,7 @@ class _PytestWrapper: decorator to issue warnings when the fixture function is called directly. """ - obj = attr.ib() + obj: Any def get_real_func(obj): @@ -284,9 +242,7 @@ def get_real_func(obj): from _pytest._io.saferepr import saferepr raise ValueError( - ("could not find real function of {start}\nstopped at {current}").format( - start=saferepr(start_obj), current=saferepr(obj) - ) + f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}" ) if isinstance(obj, functools.partial): obj = obj.func @@ -339,47 +295,25 @@ def safe_isclass(obj: object) -> bool: return False -if TYPE_CHECKING: - if sys.version_info >= (3, 8): - from typing import final as final - else: - from typing_extensions import final as final -elif sys.version_info >= (3, 8): - from typing import final as final -else: - - def final(f): - return f - - -if sys.version_info >= (3, 8): - from functools import cached_property as cached_property -else: - from typing import overload - from typing import Type - - class cached_property(Generic[_S, _T]): - __slots__ = ("func", "__doc__") - - def __init__(self, func: Callable[[_S], _T]) -> None: - self.func = func - self.__doc__ = func.__doc__ +def get_user_id() -> int | None: + """Return the current process's real user id or None if it could not be + determined. - @overload - def __get__( - self, instance: None, owner: Optional[Type[_S]] = ... - ) -> "cached_property[_S, _T]": - ... - - @overload - def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T: - ... - - def __get__(self, instance, owner=None): - if instance is None: - return self - value = instance.__dict__[self.func.__name__] = self.func(instance) - return value + :return: The user id or None if it could not be determined. + """ + # mypy follows the version and platform checking expectation of PEP 484: + # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks + # Containment checks are too complex for mypy v1.5.0 and cause failure. + if sys.platform == "win32" or sys.platform == "emscripten": + # win32 does not have a getuid() function. + # Emscripten has a return 0 stub. + return None + else: + # On other platforms, a return value of -1 is assumed to indicate that + # the current process's real user id could not be determined. + ERROR = -1 + uid = os.getuid() + return uid if uid != ERROR else None # Perform exhaustiveness checking. @@ -413,5 +347,5 @@ else: # previously. # # This also work for Enums (if you use `is` to compare) and Literals. -def assert_never(value: "NoReturn") -> "NoReturn": +def assert_never(value: NoReturn) -> NoReturn: assert False, f"Unhandled value: {value} ({type(value).__name__})" diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/__init__.py index ebf6e1b950b..3f46073ac4a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/__init__.py @@ -1,24 +1,29 @@ +# mypy: allow-untyped-defs """Command line options, ini-file and conftest.py processing.""" + import argparse import collections.abc -import contextlib import copy +import dataclasses import enum +from functools import lru_cache +import glob +import importlib.metadata import inspect import os +from pathlib import Path import re import shlex import sys -import types -import warnings -from functools import lru_cache -from pathlib import Path from textwrap import dedent -from types import TracebackType +import types +from types import FunctionType from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import Final +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -32,23 +37,26 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union +import warnings -import attr +import pluggy from pluggy import HookimplMarker +from pluggy import HookimplOpts from pluggy import HookspecMarker +from pluggy import HookspecOpts from pluggy import PluginManager -import _pytest._code -import _pytest.deprecated -import _pytest.hookspec +from .compat import PathAwareHookProxy from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError from .findpaths import determine_setup +from _pytest import __version__ +import _pytest._code from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter -from _pytest.compat import final -from _pytest.compat import importlib_metadata +import _pytest.deprecated +import _pytest.hookspec from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -56,14 +64,17 @@ from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import safe_exists from _pytest.stash import Stash from _pytest.warning_types import PytestConfigWarning +from _pytest.warning_types import warn_explicit_for -if TYPE_CHECKING: +if TYPE_CHECKING: + from .argparsing import Argument + from .argparsing import Parser from _pytest._code.code import _TracebackStyle from _pytest.terminal import TerminalReporter - from .argparsing import Argument _PluggyPlugin = object @@ -107,16 +118,14 @@ class ConftestImportFailure(Exception): def __init__( self, path: Path, - excinfo: Tuple[Type[Exception], Exception, TracebackType], + *, + cause: Exception, ) -> None: - super().__init__(path, excinfo) self.path = path - self.excinfo = excinfo + self.cause = cause def __str__(self) -> str: - return "{}: {} (from {})".format( - self.excinfo[0].__name__, self.excinfo[1], self.path - ) + return f"{type(self.cause).__name__}: {self.cause} (from {self.path})" def filter_traceback_for_conftest_import_failure( @@ -136,16 +145,20 @@ def main( ) -> Union[int, ExitCode]: """Perform an in-process test run. - :param args: List of command line arguments. + :param args: + List of command line arguments. If `None` or not given, defaults to reading + arguments directly from the process command line (:data:`sys.argv`). :param plugins: List of plugin objects to be auto-registered during initialization. :returns: An exit code. """ + old_pytest_version = os.environ.get("PYTEST_VERSION") try: + os.environ["PYTEST_VERSION"] = __version__ try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: - exc_info = ExceptionInfo.from_exc_info(e.excinfo) + exc_info = ExceptionInfo.from_exception(e.cause) tw = TerminalWriter(sys.stderr) tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) exc_info.traceback = exc_info.traceback.filter( @@ -176,6 +189,11 @@ def main( for msg in e.args: tw.line(f"ERROR: {msg}\n", red=True) return ExitCode.USAGE_ERROR + finally: + if old_pytest_version is None: + os.environ.pop("PYTEST_VERSION", None) + else: + os.environ["PYTEST_VERSION"] = old_pytest_version def console_main() -> int: @@ -231,7 +249,8 @@ essential_plugins = ( "helpconfig", # Provides -p. ) -default_plugins = essential_plugins + ( +default_plugins = ( + *essential_plugins, "python", "terminal", "debugging", @@ -243,7 +262,6 @@ default_plugins = essential_plugins + ( "monkeypatch", "recwarn", "pastebin", - "nose", "assertion", "junitxml", "doctest", @@ -256,7 +274,8 @@ default_plugins = essential_plugins + ( "logging", "reports", "python_path", - *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), + "unraisableexception", + "threadexception", "faulthandler", ) @@ -310,7 +329,9 @@ def _prepareconfig( elif isinstance(args, os.PathLike): args = [os.fspath(args)] elif not isinstance(args, list): - msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" + msg = ( # type:ignore[unreachable] + "`args` parameter expected to be a list of strings, got: {!r} (type: {})" + ) raise TypeError(msg.format(args, type(args))) config = get_config(args, plugins) @@ -339,6 +360,38 @@ def _get_directory(path: Path) -> Path: return path +def _get_legacy_hook_marks( + method: Any, + hook_type: str, + opt_names: Tuple[str, ...], +) -> Dict[str, bool]: + if TYPE_CHECKING: + # abuse typeguard from importlib to avoid massive method type union thats lacking a alias + assert inspect.isroutine(method) + known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])} + must_warn: List[str] = [] + opts: Dict[str, bool] = {} + for opt_name in opt_names: + opt_attr = getattr(method, opt_name, AttributeError) + if opt_attr is not AttributeError: + must_warn.append(f"{opt_name}={opt_attr}") + opts[opt_name] = True + elif opt_name in known_marks: + must_warn.append(f"{opt_name}=True") + opts[opt_name] = True + else: + opts[opt_name] = False + if must_warn: + hook_opts = ", ".join(must_warn) + message = _pytest.deprecated.HOOK_LEGACY_MARKING.format( + type=hook_type, + fullname=method.__qualname__, + hook_opts=hook_opts, + ) + warn_explicit_for(cast(FunctionType, method), message) + return opts + + @final class PytestPluginManager(PluginManager): """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with @@ -353,13 +406,17 @@ class PytestPluginManager(PluginManager): import _pytest.assertion super().__init__("pytest") - # The objects are module objects, only used generically. - self._conftest_plugins: Set[types.ModuleType] = set() - # State related to local conftest plugins. + # -- State related to local conftest plugins. + # All loaded conftest modules. + self._conftest_plugins: Set[types.ModuleType] = set() + # All conftest modules applicable for a directory. + # This includes the directory's own conftest modules as well + # as those of its parent directories. self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} - self._conftestpath2mod: Dict[Path, types.ModuleType] = {} + # Cutoff directory above which conftests are no longer discovered. self._confcutdir: Optional[Path] = None + # If set, conftest loading is skipped. self._noconftest = False # _getconftestmodules()'s call to _get_directory() causes a stat @@ -367,8 +424,6 @@ class PytestPluginManager(PluginManager): # session (#9478), often with the same path, so cache it. self._get_directory = lru_cache(256)(_get_directory) - self._duplicatepaths: Set[Path] = set() - # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) # previously we would issue a warning when a plugin was skipped, but @@ -398,50 +453,43 @@ class PytestPluginManager(PluginManager): # Used to know when we are importing conftests after the pytest_configure stage. self._configured = False - def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): + def parse_hookimpl_opts( + self, plugin: _PluggyPlugin, name: str + ) -> Optional[HookimplOpts]: + """:meta private:""" # pytest hooks are always prefixed with "pytest_", # so we avoid accessing possibly non-readable attributes # (see issue #1073). if not name.startswith("pytest_"): - return - # Ignore names which can not be hooks. + return None + # Ignore names which cannot be hooks. if name == "pytest_plugins": - return + return None - method = getattr(plugin, name) opts = super().parse_hookimpl_opts(plugin, name) + if opts is not None: + return opts + method = getattr(plugin, name) # Consider only actual functions for hooks (#3775). if not inspect.isroutine(method): - return - + return None # Collect unmarked hooks as long as they have the `pytest_' prefix. - if opts is None and name.startswith("pytest_"): - opts = {} - if opts is not None: - # TODO: DeprecationWarning, people should use hookimpl - # https://github.com/pytest-dev/pytest/issues/4562 - known_marks = {m.name for m in getattr(method, "pytestmark", [])} - - for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): - opts.setdefault(name, hasattr(method, name) or name in known_marks) - return opts + return _get_legacy_hook_marks( # type: ignore[return-value] + method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") + ) - def parse_hookspec_opts(self, module_or_class, name: str): + def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]: + """:meta private:""" opts = super().parse_hookspec_opts(module_or_class, name) if opts is None: method = getattr(module_or_class, name) - if name.startswith("pytest_"): - # todo: deprecate hookspec hacks - # https://github.com/pytest-dev/pytest/issues/4562 - known_marks = {m.name for m in getattr(method, "pytestmark", [])} - opts = { - "firstresult": hasattr(method, "firstresult") - or "firstresult" in known_marks, - "historic": hasattr(method, "historic") - or "historic" in known_marks, - } + opts = _get_legacy_hook_marks( # type: ignore[assignment] + method, + "spec", + ("firstresult", "historic"), + ) return opts def register( @@ -457,15 +505,19 @@ class PytestPluginManager(PluginManager): ) ) return None - ret: Optional[str] = super().register(plugin, name) - if ret: + plugin_name = super().register(plugin, name) + if plugin_name is not None: self.hook.pytest_plugin_registered.call_historic( - kwargs=dict(plugin=plugin, manager=self) + kwargs=dict( + plugin=plugin, + plugin_name=plugin_name, + manager=self, + ) ) if isinstance(plugin, types.ModuleType): self.consider_module(plugin) - return ret + return plugin_name def getplugin(self, name: str): # Support deprecated naming because plugins (xdist e.g.) use it. @@ -483,12 +535,14 @@ class PytestPluginManager(PluginManager): config.addinivalue_line( "markers", "tryfirst: mark a hook implementation function such that the " - "plugin machinery will try to call it first/as early as possible.", + "plugin machinery will try to call it first/as early as possible. " + "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.", ) config.addinivalue_line( "markers", "trylast: mark a hook implementation function such that the " - "plugin machinery will try to call it last/as late as possible.", + "plugin machinery will try to call it last/as late as possible. " + "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.", ) self._configured = True @@ -496,7 +550,16 @@ class PytestPluginManager(PluginManager): # Internal API for local conftest plugin handling. # def _set_initial_conftests( - self, namespace: argparse.Namespace, rootpath: Path + self, + args: Sequence[Union[str, Path]], + pyargs: bool, + noconftest: bool, + rootpath: Path, + confcutdir: Optional[Path], + invocation_dir: Path, + importmode: Union[ImportMode, str], + *, + consider_namespace_packages: bool, ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -505,76 +568,120 @@ class PytestPluginManager(PluginManager): All builtin and 3rd party plugins will have been loaded, however, so common options will not confuse our logic here. """ - current = Path.cwd() self._confcutdir = ( - absolutepath(current / namespace.confcutdir) - if namespace.confcutdir - else None + absolutepath(invocation_dir / confcutdir) if confcutdir else None ) - self._noconftest = namespace.noconftest - self._using_pyargs = namespace.pyargs - testpaths = namespace.file_or_dir + self._noconftest = noconftest + self._using_pyargs = pyargs foundanchor = False - for testpath in testpaths: - path = str(testpath) + for initial_path in args: + path = str(initial_path) # remove node-id syntax i = path.find("::") if i != -1: path = path[:i] - anchor = absolutepath(current / path) - if anchor.exists(): # we found some file object - self._try_load_conftest(anchor, namespace.importmode, rootpath) + anchor = absolutepath(invocation_dir / path) + + # Ensure we do not break if what appears to be an anchor + # is in fact a very long option (#10169, #11394). + if safe_exists(anchor): + self._try_load_conftest( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) foundanchor = True if not foundanchor: - self._try_load_conftest(current, namespace.importmode, rootpath) + self._try_load_conftest( + invocation_dir, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + + def _is_in_confcutdir(self, path: Path) -> bool: + """Whether to consider the given path to load conftests from.""" + if self._confcutdir is None: + return True + # The semantics here are literally: + # Do not load a conftest if it is found upwards from confcut dir. + # But this is *not* the same as: + # Load only conftests from confcutdir or below. + # At first glance they might seem the same thing, however we do support use cases where + # we want to load conftests that are not found in confcutdir or below, but are found + # in completely different directory hierarchies like packages installed + # in out-of-source trees. + # (see #9767 for a regression where the logic was inverted). + return path not in self._confcutdir.parents def _try_load_conftest( - self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path + self, + anchor: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, ) -> None: - self._getconftestmodules(anchor, importmode, rootpath) + self._loadconftestmodules( + anchor, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) # let's also consider test* subdirs if anchor.is_dir(): for x in anchor.glob("test*"): if x.is_dir(): - self._getconftestmodules(x, importmode, rootpath) + self._loadconftestmodules( + x, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) - def _getconftestmodules( - self, path: Path, importmode: Union[str, ImportMode], rootpath: Path - ) -> List[types.ModuleType]: + def _loadconftestmodules( + self, + path: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, + ) -> None: if self._noconftest: - return [] + return directory = self._get_directory(path) # Optimization: avoid repeated searches in the same directory. # Assumes always called with same importmode and rootpath. - existing_clist = self._dirpath2confmods.get(directory) - if existing_clist is not None: - return existing_clist + if directory in self._dirpath2confmods: + return - # XXX these days we may rather want to use config.rootpath - # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir. clist = [] - confcutdir_parents = self._confcutdir.parents if self._confcutdir else [] for parent in reversed((directory, *directory.parents)): - if parent in confcutdir_parents: - continue - conftestpath = parent / "conftest.py" - if conftestpath.is_file(): - mod = self._importconftest(conftestpath, importmode, rootpath) - clist.append(mod) + if self._is_in_confcutdir(parent): + conftestpath = parent / "conftest.py" + if conftestpath.is_file(): + mod = self._importconftest( + conftestpath, + importmode, + rootpath, + consider_namespace_packages=consider_namespace_packages, + ) + clist.append(mod) self._dirpath2confmods[directory] = clist - return clist + + def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]: + directory = self._get_directory(path) + return self._dirpath2confmods.get(directory, ()) def _rget_with_confmod( self, name: str, path: Path, - importmode: Union[str, ImportMode], - rootpath: Path, ) -> Tuple[types.ModuleType, Any]: - modules = self._getconftestmodules(path, importmode, rootpath=rootpath) + modules = self._getconftestmodules(path) for mod in reversed(modules): try: return mod, getattr(mod, name) @@ -583,41 +690,56 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest( - self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path + self, + conftestpath: Path, + importmode: Union[str, ImportMode], + rootpath: Path, + *, + consider_namespace_packages: bool, ) -> types.ModuleType: - # Use a resolved Path object as key to avoid loading the same conftest - # twice with build systems that create build directories containing - # symlinks to actual files. - # Using Path().resolve() is better than py.path.realpath because - # it resolves to the correct path/drive in case-insensitive file systems (#5792) - key = conftestpath.resolve() - - with contextlib.suppress(KeyError): - return self._conftestpath2mod[key] - + conftestpath_plugin_name = str(conftestpath) + existing = self.get_plugin(conftestpath_plugin_name) + if existing is not None: + return cast(types.ModuleType, existing) + + # conftest.py files there are not in a Python package all have module + # name "conftest", and thus conflict with each other. Clear the existing + # before loading the new one, otherwise the existing one will be + # returned from the module cache. pkgpath = resolve_package_path(conftestpath) if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.stem) + try: + del sys.modules[conftestpath.stem] + except KeyError: + pass try: - mod = import_path(conftestpath, mode=importmode, root=rootpath) + mod = import_path( + conftestpath, + mode=importmode, + root=rootpath, + consider_namespace_packages=consider_namespace_packages, + ) except Exception as e: assert e.__traceback__ is not None - exc_info = (type(e), e, e.__traceback__) - raise ConftestImportFailure(conftestpath, exc_info) from e + raise ConftestImportFailure(conftestpath, cause=e) from e self._check_non_top_pytest_plugins(mod, conftestpath) self._conftest_plugins.add(mod) - self._conftestpath2mod[key] = mod dirpath = conftestpath.parent if dirpath in self._dirpath2confmods: for path, mods in self._dirpath2confmods.items(): - if path and dirpath in path.parents or path == dirpath: - assert mod not in mods + if dirpath in path.parents or path == dirpath: + if mod in mods: + raise AssertionError( + f"While trying to load conftest path {conftestpath!s}, " + f"found that the module {mod} is already loaded with path {mod.__file__}. " + "This is not supposed to happen. Please report this issue to pytest." + ) mods.append(mod) self.trace(f"loading conftestmodule {mod!r}") - self.consider_conftest(mod) + self.consider_conftest(mod, registration_name=conftestpath_plugin_name) return mod def _check_non_top_pytest_plugins( @@ -666,6 +788,7 @@ class PytestPluginManager(PluginManager): parg = opt[2:] else: continue + parg = parg.strip() if exclude_only and not parg.startswith("no:"): continue self.consider_pluginarg(parg) @@ -687,18 +810,17 @@ class PytestPluginManager(PluginManager): self.set_blocked("pytest_" + name) else: name = arg - # Unblock the plugin. None indicates that it has been blocked. - # There is no interface with pluggy for this. - if self._name2plugin.get(name, -1) is None: - del self._name2plugin[name] + # Unblock the plugin. + self.unblock(name) if not name.startswith("pytest_"): - if self._name2plugin.get("pytest_" + name, -1) is None: - del self._name2plugin["pytest_" + name] + self.unblock("pytest_" + name) self.import_plugin(arg, consider_entry_points=True) - def consider_conftest(self, conftestmodule: types.ModuleType) -> None: + def consider_conftest( + self, conftestmodule: types.ModuleType, registration_name: str + ) -> None: """:meta private:""" - self.register(conftestmodule, name=conftestmodule.__file__) + self.register(conftestmodule, name=registration_name) def consider_env(self) -> None: """:meta private:""" @@ -754,7 +876,7 @@ class PytestPluginManager(PluginManager): def _get_plugin_specs_as_list( - specs: Union[None, types.ModuleType, str, Sequence[str]] + specs: Union[None, types.ModuleType, str, Sequence[str]], ) -> List[str]: """Parse a plugins specification into a list of plugin names.""" # None means empty. @@ -775,13 +897,6 @@ def _get_plugin_specs_as_list( ) -def _ensure_removed_sysmodule(modname: str) -> None: - try: - del sys.modules[modname] - except KeyError: - pass - - class Notset: def __repr__(self): return "<NOTSET>" @@ -830,7 +945,8 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: if is_simple_module: module_name, _ = os.path.splitext(fn) # we ignore "setup.py" at the root of the distribution - if module_name != "setup": + # as well as editable installation finder modules made by setuptools + if module_name != "setup" and not module_name.startswith("__editable__"): seen_some = True yield module_name elif is_package: @@ -854,10 +970,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: yield from _iter_rewritable_modules(new_package_files) -def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: - return tuple(args) - - @final class Config: """Access to configuration values, pluginmanager and plugin hooks. @@ -871,7 +983,7 @@ class Config: """ @final - @attr.s(frozen=True, auto_attribs=True) + @dataclasses.dataclass(frozen=True) class InvocationParams: """Holds parameters passed during :func:`pytest.main`. @@ -887,20 +999,46 @@ class Config: Plugins accessing ``InvocationParams`` must be aware of that. """ - args: Tuple[str, ...] = attr.ib(converter=_args_converter) + args: Tuple[str, ...] """The command-line arguments as passed to :func:`pytest.main`.""" plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] """Extra plugins, might be `None`.""" dir: Path """The directory from which :func:`pytest.main` was invoked.""" + def __init__( + self, + *, + args: Iterable[str], + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]], + dir: Path, + ) -> None: + object.__setattr__(self, "args", tuple(args)) + object.__setattr__(self, "plugins", plugins) + object.__setattr__(self, "dir", dir) + + class ArgsSource(enum.Enum): + """Indicates the source of the test arguments. + + .. versionadded:: 7.2 + """ + + #: Command line arguments. + ARGS = enum.auto() + #: Invocation directory. + INVOCATION_DIR = enum.auto() + INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias + #: 'testpaths' configuration value. + TESTPATHS = enum.auto() + def __init__( self, pluginmanager: PytestPluginManager, *, invocation_params: Optional[InvocationParams] = None, ) -> None: - from .argparsing import Parser, FILE_OR_DIR + from .argparsing import FILE_OR_DIR + from .argparsing import Parser if invocation_params is None: invocation_params = self.InvocationParams( @@ -940,10 +1078,8 @@ class Config: # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash - from .compat import PathAwareHookProxy - self.trace = self.pluginmanager.trace.root.get("config") - self.hook = PathAwareHookProxy(self.pluginmanager.hook) + self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] self._inicache: Dict[str, Any] = {} self._override_ini: Sequence[str] = () self._opt2dest: Dict[str, str] = {} @@ -953,6 +1089,8 @@ class Config: self.hook.pytest_addoption.call_historic( kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) ) + self.args_source = Config.ArgsSource.ARGS + self.args: List[str] = [] if TYPE_CHECKING: from _pytest.cacheprovider import Cache @@ -1001,9 +1139,10 @@ class Config: fin() def get_terminal_writer(self) -> TerminalWriter: - terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( + terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin( "terminalreporter" ) + assert terminalreporter is not None return terminalreporter._tw def pytest_cmdline_parse( @@ -1012,7 +1151,6 @@ class Config: try: self.parse(args) except UsageError: - # Handle --version and --help here in a minimal fashion. # This gets done via helpconfig normally, but its # pytest_cmdline_main is not called in case of errors. @@ -1077,8 +1215,29 @@ class Config: @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: + # We haven't fully parsed the command line arguments yet, so + # early_config.args it not set yet. But we need it for + # discovering the initial conftests. So "pre-run" the logic here. + # It will be done for real in `parse()`. + args, args_source = early_config._decide_args( + args=early_config.known_args_namespace.file_or_dir, + pyargs=early_config.known_args_namespace.pyargs, + testpaths=early_config.getini("testpaths"), + invocation_dir=early_config.invocation_params.dir, + rootpath=early_config.rootpath, + warn=False, + ) self.pluginmanager._set_initial_conftests( - early_config.known_args_namespace, rootpath=early_config.rootpath + args=args, + pyargs=early_config.known_args_namespace.pyargs, + noconftest=early_config.known_args_namespace.noconftest, + rootpath=early_config.rootpath, + confcutdir=early_config.known_args_namespace.confcutdir, + invocation_dir=early_config.invocation_params.dir, + importmode=early_config.known_args_namespace.importmode, + consider_namespace_packages=early_config.getini( + "consider_namespace_packages" + ), ) def _initini(self, args: Sequence[str]) -> None: @@ -1086,21 +1245,21 @@ class Config: args, namespace=copy.copy(self.option) ) rootpath, inipath, inicfg = determine_setup( - ns.inifilename, - ns.file_or_dir + unknown_args, + inifile=ns.inifilename, + args=ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, - config=self, + invocation_dir=self.invocation_params.dir, ) self._rootpath = rootpath self._inipath = inipath self.inicfg = inicfg self._parser.extra_info["rootdir"] = str(self.rootpath) self._parser.extra_info["inifile"] = str(self.inipath) - self._parser.addini("addopts", "extra command line options", "args") - self._parser.addini("minversion", "minimally required pytest version") + self._parser.addini("addopts", "Extra command line options", "args") + self._parser.addini("minversion", "Minimally required pytest version") self._parser.addini( "required_plugins", - "plugins that must be present for pytest to run", + "Plugins that must be present for pytest to run", type="args", default=[], ) @@ -1138,7 +1297,7 @@ class Config: package_files = ( str(file) - for dist in importlib_metadata.distributions() + for dist in importlib.metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) @@ -1158,6 +1317,51 @@ class Config: return args + def _decide_args( + self, + *, + args: List[str], + pyargs: bool, + testpaths: List[str], + invocation_dir: Path, + rootpath: Path, + warn: bool, + ) -> Tuple[List[str], ArgsSource]: + """Decide the args (initial paths/nodeids) to use given the relevant inputs. + + :param warn: Whether can issue warnings. + + :returns: The args and the args source. Guaranteed to be non-empty. + """ + if args: + source = Config.ArgsSource.ARGS + result = args + else: + if invocation_dir == rootpath: + source = Config.ArgsSource.TESTPATHS + if pyargs: + result = testpaths + else: + result = [] + for path in testpaths: + result.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not result: + if warn: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) + else: + result = [] + if not result: + source = Config.ArgsSource.INVOCATION_DIR + result = [str(invocation_dir)] + return result, source + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") @@ -1191,13 +1395,11 @@ class Config: self._validate_plugins() self._warn_about_skipped_plugins() - if self.known_args_namespace.strict: - self.issue_config_time_warning( - _pytest.deprecated.STRICT_OPTION, stacklevel=2 - ) - - if self.known_args_namespace.confcutdir is None and self.inipath is not None: - confcutdir = str(self.inipath.parent) + if self.known_args_namespace.confcutdir is None: + if self.inipath is not None: + confcutdir = str(self.inipath.parent) + else: + confcutdir = str(self.rootpath) self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests( @@ -1214,12 +1416,14 @@ class Config: else: raise - @hookimpl(hookwrapper=True) - def pytest_collection(self) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_collection(self) -> Generator[None, object, object]: # Validate invalid ini keys after collection is done so we take in account # options added by late-loading conftest files. - yield - self._validate_config_options() + try: + return (yield) + finally: + self._validate_config_options() def _checkversion(self) -> None: import pytest @@ -1236,12 +1440,7 @@ class Config: if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( - "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % ( - self.inipath, - minver, - pytest.__version__, - ) + f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'" ) def _validate_config_options(self) -> None: @@ -1254,8 +1453,9 @@ class Config: return # Imported lazily to improve start-up time. + from packaging.requirements import InvalidRequirement + from packaging.requirements import Requirement from packaging.version import Version - from packaging.requirements import InvalidRequirement, Requirement plugin_info = self.pluginmanager.list_plugin_distinfo() plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} @@ -1292,26 +1492,26 @@ class Config: def parse(self, args: List[str], addopts: bool = True) -> None: # Parse given cmdline arguments into this config object. - assert not hasattr( - self, "args" + assert ( + self.args == [] ), "can only parse cmdline args at most once per Config object" self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager) ) self._preparse(args, addopts=addopts) - # XXX deprecated hook: - self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.after_preparse = True # type: ignore try: args = self._parser.parse_setoption( args, self.option, namespace=self.option ) - if not args: - if self.invocation_params.dir == self.rootpath: - args = self.getini("testpaths") - if not args: - args = [str(self.invocation_params.dir)] - self.args = args + self.args, self.args_source = self._decide_args( + args=args, + pyargs=self.known_args_namespace.pyargs, + testpaths=self.getini("testpaths"), + invocation_dir=self.invocation_params.dir, + rootpath=self.rootpath, + warn=True, + ) except PrintHelp: pass @@ -1319,7 +1519,7 @@ class Config: """Issue and handle a warning during the "configure" stage. During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` - function because it is not possible to have hookwrappers around ``pytest_configure``. + function because it is not possible to have hook wrappers around ``pytest_configure``. This function is mainly intended for plugins that need to issue warnings during ``pytest_configure`` (or similar stages). @@ -1341,14 +1541,6 @@ class Config: if records: frame = sys._getframe(stacklevel - 1) location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name - self.hook.pytest_warning_captured.call_historic( - kwargs=dict( - warning_message=records[0], - when="config", - item=None, - location=location, - ) - ) self.hook.pytest_warning_recorded.call_historic( kwargs=dict( warning_message=records[0], @@ -1369,6 +1561,27 @@ class Config: def getini(self, name: str): """Return configuration value from an :ref:`ini file <configfiles>`. + If a configuration value is not defined in an + :ref:`ini file <configfiles>`, then the ``default`` value provided while + registering the configuration through + :func:`parser.addini <pytest.Parser.addini>` will be returned. + Please note that you can even provide ``None`` as a valid + default value. + + If ``default`` is not provided while registering using + :func:`parser.addini <pytest.Parser.addini>`, then a default value + based on the ``type`` parameter passed to + :func:`parser.addini <pytest.Parser.addini>` will be returned. + The default values based on ``type`` are: + ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]`` + ``bool`` : ``False`` + ``string`` : empty string ``""`` + + If neither the ``default`` nor the ``type`` parameter is passed + while registering the configuration through + :func:`parser.addini <pytest.Parser.addini>`, then the configuration + is treated as a string and a default empty string '' is returned. + If the specified name hasn't been registered through a prior :func:`parser.addini <pytest.Parser.addini>` call (usually from a plugin), a ValueError is raised. @@ -1395,11 +1608,7 @@ class Config: try: value = self.inicfg[name] except KeyError: - if default is not None: - return default - if type is None: - return "" - return [] + return default else: value = override_value # Coerce the values based on types. @@ -1418,9 +1627,11 @@ class Config: # in this case, we already have a list ready to use. # if type == "paths": - # TODO: This assert is probably not valid in all cases. - assert self.inipath is not None - dp = self.inipath.parent + dp = ( + self.inipath.parent + if self.inipath is not None + else self.invocation_params.dir + ) input_values = shlex.split(value) if isinstance(value, str) else value return [dp / x for x in input_values] elif type == "args": @@ -1439,15 +1650,12 @@ class Config: else: return self._getini_unknown_type(name, type, value) - def _getconftest_pathlist( - self, name: str, path: Path, rootpath: Path - ) -> Optional[List[Path]]: + def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]: try: - mod, relroots = self.pluginmanager._rget_with_confmod( - name, path, self.getoption("importmode"), rootpath - ) + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None + assert mod.__file__ is not None modpath = Path(mod.__file__).parent values: List[Path] = [] for relroot in relroots: @@ -1469,9 +1677,7 @@ class Config: key, user_ini_value = ini_config.split("=", 1) except ValueError as e: raise UsageError( - "-o/--override-ini expects option=value style (got: {!r}).".format( - ini_config - ) + f"-o/--override-ini expects option=value style (got: {ini_config!r})." ) from e else: if key == name: @@ -1510,6 +1716,79 @@ class Config: """Deprecated, use getoption(skip=True) instead.""" return self.getoption(name, skip=True) + #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`). + VERBOSITY_ASSERTIONS: Final = "assertions" + #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`). + VERBOSITY_TEST_CASES: Final = "test_cases" + _VERBOSITY_INI_DEFAULT: Final = "auto" + + def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: + r"""Retrieve the verbosity level for a fine-grained verbosity type. + + :param verbosity_type: Verbosity type to get level for. If a level is + configured for the given type, that value will be returned. If the + given type is not a known verbosity type, the global verbosity + level will be returned. If the given type is None (default), the + global verbosity level will be returned. + + To configure a level for a fine-grained verbosity type, the + configuration file should have a setting for the configuration name + and a numeric value for the verbosity level. A special value of "auto" + can be used to explicitly use the global verbosity level. + + Example: + .. code-block:: ini + + # content of pytest.ini + [pytest] + verbosity_assertions = 2 + + .. code-block:: console + + pytest -v + + .. code-block:: python + + print(config.get_verbosity()) # 1 + print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2 + """ + global_level = self.option.verbose + assert isinstance(global_level, int) + if verbosity_type is None: + return global_level + + ini_name = Config._verbosity_ini_name(verbosity_type) + if ini_name not in self._parser._inidict: + return global_level + + level = self.getini(ini_name) + if level == Config._VERBOSITY_INI_DEFAULT: + return global_level + + return int(level) + + @staticmethod + def _verbosity_ini_name(verbosity_type: str) -> str: + return f"verbosity_{verbosity_type}" + + @staticmethod + def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None: + """Add a output verbosity configuration option for the given output type. + + :param parser: Parser for command line arguments and ini-file values. + :param verbosity_type: Fine-grained verbosity category. + :param help: Description of the output this type controls. + + The value should be retrieved via a call to + :py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`. + """ + parser.addini( + Config._verbosity_ini_name(verbosity_type), + help=help, + type="string", + default=Config._VERBOSITY_INI_DEFAULT, + ) + def _warn_about_missing_assertion(self, mode: str) -> None: if not _assertion_supported(): if mode == "plain": @@ -1593,7 +1872,7 @@ def _strtobool(val: str) -> bool: @lru_cache(maxsize=50) def parse_warning_filter( arg: str, *, escape: bool -) -> Tuple[str, str, Type[Warning], str, int]: +) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]: """Parse a warnings filter string. This is copied from warnings._setoption with the following changes: @@ -1635,15 +1914,15 @@ def parse_warning_filter( parts.append("") action_, message, category_, module, lineno_ = (s.strip() for s in parts) try: - action: str = warnings._getaction(action_) # type: ignore[attr-defined] + action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined] except warnings._OptionError as e: - raise UsageError(error_template.format(error=str(e))) + raise UsageError(error_template.format(error=str(e))) from None try: category: Type[Warning] = _resolve_warning_category(category_) except Exception: exc_info = ExceptionInfo.from_current() exception_text = exc_info.getrepr(style="native") - raise UsageError(error_template.format(error=exception_text)) + raise UsageError(error_template.format(error=exception_text)) from None if message and escape: message = re.escape(message) if module and escape: @@ -1656,7 +1935,7 @@ def parse_warning_filter( except ValueError as e: raise UsageError( error_template.format(error=f"invalid lineno {lineno_!r}: {e}") - ) + ) from None else: lineno = 0 return action, message, category, module, lineno diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py index b0bb3f168ff..9006351af72 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/argparsing.py @@ -1,35 +1,38 @@ +# mypy: allow-untyped-defs import argparse +from gettext import gettext import os import sys -import warnings -from gettext import gettext from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import List +from typing import Literal from typing import Mapping +from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import _pytest._io -from _pytest.compat import final from _pytest.config.exceptions import UsageError -from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT -from _pytest.deprecated import ARGUMENT_TYPE_STR -from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE from _pytest.deprecated import check_ispytest -if TYPE_CHECKING: - from typing import NoReturn - from typing_extensions import Literal FILE_OR_DIR = "file_or_dir" +class NotSet: + def __repr__(self) -> str: + return "<notset>" + + +NOT_SET = NotSet() + + @final class Parser: """Parser for command line arguments and ini-file values. @@ -48,7 +51,7 @@ class Parser: _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - self._anonymous = OptionGroup("custom options", parser=self, _ispytest=True) + self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True) self._groups: List[OptionGroup] = [] self._processopt = processopt self._usage = usage @@ -66,14 +69,15 @@ class Parser: ) -> "OptionGroup": """Get (or create) a named option Group. - :name: Name of the option group. - :description: Long description for --help output. - :after: Name of another group, used for ordering --help output. + :param name: Name of the option group. + :param description: Long description for --help output. + :param after: Name of another group, used for ordering --help output. + :returns: The option group. The returned group object has an ``addoption`` method with the same signature as :func:`parser.addoption <pytest.Parser.addoption>` but will be shown in the respective group in the output of - ``pytest. --help``. + ``pytest --help``. """ for group in self._groups: if group.name == name: @@ -89,10 +93,11 @@ class Parser: def addoption(self, *opts: str, **attrs: Any) -> None: """Register a command line option. - :opts: Option names, can be short or long options. - :attrs: Same attributes which the ``add_argument()`` function of the - `argparse library <https://docs.python.org/library/argparse.html>`_ - accepts. + :param opts: + Option names, can be short or long options. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + <argparse.ArgumentParser.add_argument>` function accepts. After command line parsing, options are available on the pytest config object via ``config.option.NAME`` where ``NAME`` is usually set @@ -117,7 +122,7 @@ class Parser: from _pytest._argcomplete import filescompleter optparser = MyOptionParser(self, self.extra_info, prog=self.prog) - groups = self._groups + [self._anonymous] + groups = [*self._groups, self._anonymous] for group in groups: if group.options: desc = group.description or group.name @@ -148,7 +153,10 @@ class Parser: args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: - """Parse and return a namespace object with known arguments at this point.""" + """Parse the known arguments at this point. + + :returns: An argparse namespace object. + """ return self.parse_known_and_unknown_args(args, namespace=namespace)[0] def parse_known_and_unknown_args( @@ -156,8 +164,13 @@ class Parser: args: Sequence[Union[str, "os.PathLike[str]"]], namespace: Optional[argparse.Namespace] = None, ) -> Tuple[argparse.Namespace, List[str]]: - """Parse and return a namespace object with known arguments, and - the remaining arguments unknown at this point.""" + """Parse the known arguments at this point, and also return the + remaining unknown arguments. + + :returns: + A tuple containing an argparse namespace object for the known + arguments, and a list of the unknown arguments. + """ optparser = self._getparser() strargs = [os.fspath(x) for x in args] return optparser.parse_known_args(strargs, namespace=namespace) @@ -167,15 +180,15 @@ class Parser: name: str, help: str, type: Optional[ - "Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']" + Literal["string", "paths", "pathlist", "args", "linelist", "bool"] ] = None, - default=None, + default: Any = NOT_SET, ) -> None: """Register an ini-file option. - :name: + :param name: Name of the ini-variable. - :type: + :param type: Type of the variable. Can be: * ``string``: a string @@ -185,21 +198,48 @@ class Parser: * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell * ``pathlist``: a list of ``py.path``, separated as in a shell + For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file. + In case the execution is happening without an ini-file defined, + they will be considered relative to the current working directory (for example with ``--override-ini``). + .. versionadded:: 7.0 The ``paths`` variable type. + .. versionadded:: 8.1 + Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file. + Defaults to ``string`` if ``None`` or not passed. - :default: + :param default: Default value if no ini-file option exists but is queried. The value of ini-variables can be retrieved via a call to :py:func:`config.getini(name) <pytest.Config.getini>`. """ assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool") + if default is NOT_SET: + default = get_ini_default_for_type(type) + self._inidict[name] = (help, type, default) self._ininames.append(name) +def get_ini_default_for_type( + type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]], +) -> Any: + """ + Used by addini to get the default value for a given ini-option type, when + default is not supplied. + """ + if type is None: + return "" + elif type in ("paths", "pathlist", "args", "linelist"): + return [] + elif type == "bool": + return False + else: + return "" + + class ArgumentError(Exception): """Raised if an Argument instance is created with invalid or inconsistent arguments.""" @@ -224,39 +264,15 @@ class Argument: https://docs.python.org/3/library/optparse.html#optparse-standard-option-types """ - _typ_map = {"int": int, "string": str, "float": float, "complex": complex} - def __init__(self, *names: str, **attrs: Any) -> None: - """Store parms in private vars for use in add_argument.""" + """Store params in private vars for use in add_argument.""" self._attrs = attrs self._short_opts: List[str] = [] self._long_opts: List[str] = [] - if "%default" in (attrs.get("help") or ""): - warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3) try: - typ = attrs["type"] + self.type = attrs["type"] except KeyError: pass - else: - # This might raise a keyerror as well, don't want to catch that. - if isinstance(typ, str): - if typ == "choice": - warnings.warn( - ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names), - stacklevel=4, - ) - # argparse expects a type here take it from - # the type of the first element - attrs["type"] = type(attrs["choices"][0]) - else: - warnings.warn( - ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4 - ) - attrs["type"] = Argument._typ_map[typ] - # Used in test_parseopt -> test_parse_defaultgetter. - self.type = attrs["type"] - else: - self.type = typ try: # Attribute existence is tested in Config._processopt. self.default = attrs["default"] @@ -287,11 +303,6 @@ class Argument: self._attrs[attr] = getattr(self, attr) except AttributeError: pass - if self._attrs.get("help"): - a = self._attrs["help"] - a = a.replace("%default", "%(default)s") - # a = a.replace('%prog', '%(prog)s') - self._attrs["help"] = a return self._attrs def _set_opt_strings(self, opts: Sequence[str]) -> None: @@ -354,24 +365,30 @@ class OptionGroup: self.options: List[Argument] = [] self.parser = parser - def addoption(self, *optnames: str, **attrs: Any) -> None: + def addoption(self, *opts: str, **attrs: Any) -> None: """Add an option to this group. If a shortened version of a long option is specified, it will be suppressed in the help. ``addoption('--twowords', '--two-words')`` results in help showing ``--two-words`` only, but ``--twowords`` gets accepted **and** the automatic destination is in ``args.twowords``. + + :param opts: + Option names, can be short or long options. + :param attrs: + Same attributes as the argparse library's :meth:`add_argument() + <argparse.ArgumentParser.add_argument>` function accepts. """ - conflict = set(optnames).intersection( + conflict = set(opts).intersection( name for opt in self.options for name in opt.names() ) if conflict: raise ValueError("option names %s already added" % conflict) - option = Argument(*optnames, **attrs) + option = Argument(*opts, **attrs) self._addoption_instance(option, shortupper=False) - def _addoption(self, *optnames: str, **attrs: Any) -> None: - option = Argument(*optnames, **attrs) + def _addoption(self, *opts: str, **attrs: Any) -> None: + option = Argument(*opts, **attrs) self._addoption_instance(option, shortupper=True) def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: @@ -398,18 +415,18 @@ class MyOptionParser(argparse.ArgumentParser): add_help=False, formatter_class=DropShorterLongHelpFormatter, allow_abbrev=False, + fromfile_prefix_chars="@", ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user. self.extra_info = extra_info if extra_info else {} - def error(self, message: str) -> "NoReturn": + def error(self, message: str) -> NoReturn: """Transform argparse error message into UsageError.""" msg = f"{self.prog}: error: {message}" if hasattr(self._parser, "_config_source_hint"): - # Type ignored because the attribute is set dynamically. - msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore + msg = f"{msg} ({self._parser._config_source_hint})" raise UsageError(self.format_usage() + msg) @@ -431,7 +448,7 @@ class MyOptionParser(argparse.ArgumentParser): getattr(parsed, FILE_OR_DIR).extend(unrecognized) return parsed - if sys.version_info[:2] < (3, 9): # pragma: no cover + if sys.version_info < (3, 9): # pragma: no cover # Backport of https://github.com/python/cpython/pull/14316 so we can # disable long --argument abbreviations without breaking short flags. def _parse_optional( @@ -439,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser): ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: if not arg_string: return None - if not arg_string[0] in self.prefix_chars: + if arg_string[0] not in self.prefix_chars: return None if arg_string in self._option_string_actions: action = self._option_string_actions[arg_string] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/compat.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/compat.py index ba267d21505..2856d85d195 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/compat.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/compat.py @@ -1,15 +1,20 @@ +from __future__ import annotations + import functools -import warnings from pathlib import Path -from typing import Optional +from typing import Any +from typing import Mapping +import warnings + +import pluggy from ..compat import LEGACY_PATH from ..compat import legacy_path from ..deprecated import HOOK_LEGACY_PATH_ARG -from _pytest.nodes import _check_path + # hookname: (Path, LEGACY_PATH) -imply_paths_hooks = { +imply_paths_hooks: Mapping[str, tuple[str, str]] = { "pytest_ignore_collect": ("collection_path", "path"), "pytest_collect_file": ("file_path", "path"), "pytest_pycollect_makemodule": ("module_path", "path"), @@ -18,6 +23,14 @@ imply_paths_hooks = { } +def _check_path(path: Path, fspath: LEGACY_PATH) -> None: + if Path(fspath) != path: + raise ValueError( + f"Path({fspath!r}) != {path!r}\n" + "if both path and fspath are given they need to be equal" + ) + + class PathAwareHookProxy: """ this helper wraps around hook callers @@ -27,25 +40,24 @@ class PathAwareHookProxy: this may have to be changed later depending on bugs """ - def __init__(self, hook_caller): - self.__hook_caller = hook_caller + def __init__(self, hook_relay: pluggy.HookRelay) -> None: + self._hook_relay = hook_relay - def __dir__(self): - return dir(self.__hook_caller) + def __dir__(self) -> list[str]: + return dir(self._hook_relay) - def __getattr__(self, key, _wraps=functools.wraps): - hook = getattr(self.__hook_caller, key) + def __getattr__(self, key: str) -> pluggy.HookCaller: + hook: pluggy.HookCaller = getattr(self._hook_relay, key) if key not in imply_paths_hooks: self.__dict__[key] = hook return hook else: path_var, fspath_var = imply_paths_hooks[key] - @_wraps(hook) - def fixed_hook(**kw): - - path_value: Optional[Path] = kw.pop(path_var, None) - fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) + @functools.wraps(hook) + def fixed_hook(**kw: Any) -> Any: + path_value: Path | None = kw.pop(path_var, None) + fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None) if fspath_value is not None: warnings.warn( HOOK_LEGACY_PATH_ARG.format( @@ -66,6 +78,8 @@ class PathAwareHookProxy: kw[fspath_var] = fspath_value return hook(**kw) + fixed_hook.name = hook.name # type: ignore[attr-defined] + fixed_hook.spec = hook.spec # type: ignore[attr-defined] fixed_hook.__name__ = key self.__dict__[key] = fixed_hook - return fixed_hook + return fixed_hook # type: ignore[return-value] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py index 4f1320e758d..4031ea732f3 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/exceptions.py @@ -1,4 +1,4 @@ -from _pytest.compat import final +from typing import final @final diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py index 89ade5f23b9..9909376de0f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/config/findpaths.py @@ -1,12 +1,12 @@ import os from pathlib import Path +import sys from typing import Dict from typing import Iterable from typing import List from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import iniconfig @@ -15,9 +15,7 @@ from .exceptions import UsageError from _pytest.outcomes import fail from _pytest.pathlib import absolutepath from _pytest.pathlib import commonpath - -if TYPE_CHECKING: - from . import Config +from _pytest.pathlib import safe_exists def _parse_ini_config(path: Path) -> iniconfig.IniConfig: @@ -39,7 +37,6 @@ def load_config_dict_from_file( Return None if the file does not contain valid pytest configuration. """ - # Configuration from ini files are obtained from the [pytest] section, if present. if filepath.suffix == ".ini": iniconfig = _parse_ini_config(filepath) @@ -64,13 +61,16 @@ def load_config_dict_from_file( # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. elif filepath.suffix == ".toml": - import tomli + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib toml_text = filepath.read_text(encoding="utf-8") try: - config = tomli.loads(toml_text) - except tomli.TOMLDecodeError as exc: - raise UsageError(str(exc)) from exc + config = tomllib.loads(toml_text) + except tomllib.TOMLDecodeError as exc: + raise UsageError(f"{filepath}: {exc}") from exc result = config.get("tool", {}).get("pytest", {}).get("ini_options", None) if result is not None: @@ -86,32 +86,42 @@ def load_config_dict_from_file( def locate_config( + invocation_dir: Path, args: Iterable[Path], ) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]: """Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict).""" config_names = [ "pytest.ini", + ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg", ] args = [x for x in args if not str(x).startswith("-")] if not args: - args = [Path.cwd()] + args = [invocation_dir] + found_pyproject_toml: Optional[Path] = None for arg in args: argpath = absolutepath(arg) for base in (argpath, *argpath.parents): for config_name in config_names: p = base / config_name if p.is_file(): + if p.name == "pyproject.toml" and found_pyproject_toml is None: + found_pyproject_toml = p ini_config = load_config_dict_from_file(p) if ini_config is not None: return base, p, ini_config + if found_pyproject_toml is not None: + return found_pyproject_toml.parent, found_pyproject_toml, {} return None, None, {} -def get_common_ancestor(paths: Iterable[Path]) -> Path: +def get_common_ancestor( + invocation_dir: Path, + paths: Iterable[Path], +) -> Path: common_ancestor: Optional[Path] = None for path in paths: if not path.exists(): @@ -128,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path: if shared is not None: common_ancestor = shared if common_ancestor is None: - common_ancestor = Path.cwd() + common_ancestor = invocation_dir elif common_ancestor.is_file(): common_ancestor = common_ancestor.parent return common_ancestor @@ -146,14 +156,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]: return path return path.parent - def safe_exists(path: Path) -> bool: - # This can throw on paths that contain characters unrepresentable at the OS level, - # or with invalid syntax on Windows (https://bugs.python.org/issue35306) - try: - return path.exists() - except OSError: - return False - # These look like paths but may not exist possible_paths = ( absolutepath(get_file_part_from_node_id(arg)) @@ -168,11 +170,24 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte def determine_setup( + *, inifile: Optional[str], args: Sequence[str], - rootdir_cmd_arg: Optional[str] = None, - config: Optional["Config"] = None, + rootdir_cmd_arg: Optional[str], + invocation_dir: Path, ) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: + """Determine the rootdir, inifile and ini configuration values from the + command line arguments. + + :param inifile: + The `--inifile` command line argument, if given. + :param args: + The free command line arguments. + :param rootdir_cmd_arg: + The `--rootdir` command line argument, if given. + :param invocation_dir: + The working directory when pytest was invoked. + """ rootdir = None dirs = get_dirs_from_args(args) if inifile: @@ -182,8 +197,8 @@ def determine_setup( if rootdir_cmd_arg is None: rootdir = inipath_.parent else: - ancestor = get_common_ancestor(dirs) - rootdir, inipath, inicfg = locate_config([ancestor]) + ancestor = get_common_ancestor(invocation_dir, dirs) + rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in (ancestor, *ancestor.parents): if (possible_rootdir / "setup.py").is_file(): @@ -191,23 +206,26 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inipath, inicfg = locate_config(dirs) + rootdir, inipath, inicfg = locate_config(invocation_dir, dirs) if rootdir is None: - if config is not None: - cwd = config.invocation_params.dir - else: - cwd = Path.cwd() - rootdir = get_common_ancestor([cwd, ancestor]) - is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" - if is_fs_root: + rootdir = get_common_ancestor( + invocation_dir, [invocation_dir, ancestor] + ) + if is_fs_root(rootdir): rootdir = ancestor if rootdir_cmd_arg: rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) if not rootdir.is_dir(): raise UsageError( - "Directory '{}' not found. Check your '--rootdir' option.".format( - rootdir - ) + f"Directory '{rootdir}' not found. Check your '--rootdir' option." ) assert rootdir is not None return rootdir, inipath, inicfg or {} + + +def is_fs_root(p: Path) -> bool: + r""" + Return True if the given path is pointing to the root of the + file system ("/" on Unix and "C:\\" on Windows for example). + """ + return os.path.splitdrive(str(p))[1] == os.sep diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/debugging.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/debugging.py index 452fb18ac34..6ed0c5c7aee 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/debugging.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/debugging.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Interactive debugging with PDB, the Python Debugger.""" + import argparse import functools import sys @@ -12,6 +14,7 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union +import unittest from _pytest import outcomes from _pytest._code import ExceptionInfo @@ -24,6 +27,7 @@ from _pytest.config.exceptions import UsageError from _pytest.nodes import Node from _pytest.reports import BaseReport + if TYPE_CHECKING: from _pytest.capture import CaptureManager from _pytest.runner import CallInfo @@ -46,21 +50,21 @@ def pytest_addoption(parser: Parser) -> None: "--pdb", dest="usepdb", action="store_true", - help="start the interactive Python debugger on errors or KeyboardInterrupt.", + help="Start the interactive Python debugger on errors or KeyboardInterrupt", ) group._addoption( "--pdbcls", dest="usepdb_cls", metavar="modulename:classname", type=_validate_usepdb_cls, - help="specify a custom interactive Python debugger for use with --pdb." + help="Specify a custom interactive Python debugger for use with --pdb." "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", ) group._addoption( "--trace", dest="trace", action="store_true", - help="Immediately break when running each test.", + help="Immediately break when running each test", ) @@ -151,9 +155,7 @@ class pytestPDB: def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): import _pytest.config - # Type ignored because mypy doesn't support "dynamic" - # inheritance like this. - class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] + class PytestPdbWrapper(pdb_cls): _pytest_capman = capman _continued = False @@ -262,8 +264,7 @@ class pytestPDB: elif capturing: tw.sep( ">", - "PDB %s (IO-capturing turned off for %s)" - % (method, capturing), + f"PDB {method} (IO-capturing turned off for {capturing})", ) else: tw.sep(">", f"PDB {method}") @@ -293,7 +294,9 @@ class PdbInvoke: sys.stdout.write(out) sys.stdout.write(err) assert call.excinfo is not None - _enter_pdb(node, call.excinfo, report) + + if not isinstance(call.excinfo.value, unittest.SkipTest): + _enter_pdb(node, call.excinfo, report) def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: tb = _postmortem_traceback(excinfo) @@ -301,10 +304,10 @@ class PdbInvoke: class PdbTrace: - @hookimpl(hookwrapper=True) - def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: wrap_pytest_function_for_tracing(pyfuncitem) - yield + return (yield) def wrap_pytest_function_for_tracing(pyfuncitem): @@ -374,7 +377,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb elif isinstance(excinfo.value, ConftestImportFailure): # A config.ConftestImportFailure is not useful for post_mortem. # Use the underlying exception instead: - return excinfo.value.excinfo[2] + assert excinfo.value.cause.__traceback__ is not None + return excinfo.value.cause.__traceback__ else: assert excinfo._excinfo is not None return excinfo._excinfo[2] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/deprecated.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/deprecated.py index 5248927113e..10811d158aa 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/deprecated.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/deprecated.py @@ -8,13 +8,14 @@ All constants defined in this module should be either instances of :class:`PytestWarning`, or :class:`UnformattedWarning` in case of warnings which need to format their messages. """ + from warnings import warn from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import PytestRemovedIn7Warning -from _pytest.warning_types import PytestRemovedIn8Warning +from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import UnformattedWarning + # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts DEPRECATED_EXTERNAL_PLUGINS = { @@ -24,18 +25,6 @@ DEPRECATED_EXTERNAL_PLUGINS = { } -FILLFUNCARGS = UnformattedWarning( - PytestRemovedIn7Warning, - "{name} is deprecated, use " - "function._request._fillfixtures() instead if you cannot avoid reaching into internals.", -) - -PYTEST_COLLECT_MODULE = UnformattedWarning( - PytestRemovedIn7Warning, - "pytest.collect.{name} was moved to pytest.{name}\n" - "Please update to the new name.", -) - # This can be* removed pytest 8, but it's harmless and common, so no rush to remove. # * If you're in the future: "could have been". YIELD_FIXTURE = PytestDeprecationWarning( @@ -43,92 +32,37 @@ YIELD_FIXTURE = PytestDeprecationWarning( "Use @pytest.fixture instead; they are the same." ) -MINUS_K_DASH = PytestRemovedIn7Warning( - "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead." -) - -MINUS_K_COLON = PytestRemovedIn7Warning( - "The `-k 'expr:'` syntax to -k is deprecated.\n" - "Please open an issue if you use this and want a replacement." -) - -WARNING_CAPTURED_HOOK = PytestRemovedIn7Warning( - "The pytest_warning_captured is deprecated and will be removed in a future release.\n" - "Please use pytest_warning_recorded instead." -) - -WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning( - "The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n" - "Please use pytest_load_initial_conftests hook instead." -) - -FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning( - "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; " - "use self.session.gethookproxy() and self.session.isinitpath() instead. " -) - -STRICT_OPTION = PytestRemovedIn8Warning( - "The --strict option is deprecated, use --strict-markers instead." -) - # This deprecation is never really meant to be removed. PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") -UNITTEST_SKIP_DURING_COLLECTION = PytestRemovedIn8Warning( - "Raising unittest.SkipTest to skip tests during collection is deprecated. " - "Use pytest.skip() instead." -) - -ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning( - 'pytest now uses argparse. "%default" should be changed to "%(default)s"', -) - -ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning( - PytestRemovedIn8Warning, - "`type` argument to addoption() is the string {typ!r}." - " For choices this is optional and can be omitted, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: {names})", -) - -ARGUMENT_TYPE_STR = UnformattedWarning( - PytestRemovedIn8Warning, - "`type` argument to addoption() is the string {typ!r}, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: {names})", -) - HOOK_LEGACY_PATH_ARG = UnformattedWarning( - PytestRemovedIn8Warning, + PytestRemovedIn9Warning, "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n" "see https://docs.pytest.org/en/latest/deprecations.html" "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", ) NODE_CTOR_FSPATH_ARG = UnformattedWarning( - PytestRemovedIn8Warning, + PytestRemovedIn9Warning, "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " "Please use the (path: pathlib.Path) argument instead.\n" "See https://docs.pytest.org/en/latest/deprecations.html" "#fspath-argument-for-node-constructors-replaced-with-pathlib-path", ) -WARNS_NONE_ARG = PytestRemovedIn8Warning( - "Passing None has been deprecated.\n" - "See https://docs.pytest.org/en/latest/how-to/capture-warnings.html" - "#additional-use-cases-of-warnings-in-tests" - " for alternatives in common use cases." -) - -KEYWORD_MSG_ARG = UnformattedWarning( - PytestRemovedIn8Warning, - "pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead", +HOOK_LEGACY_MARKING = UnformattedWarning( + PytestDeprecationWarning, + "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n" + "Please use the pytest.hook{type}({hook_opts}) decorator instead\n" + " to configure the hooks.\n" + " See https://docs.pytest.org/en/latest/deprecations.html" + "#configuring-hook-specs-impls-using-markers", ) -INSTANCE_COLLECTOR = PytestRemovedIn8Warning( - "The pytest.Instance collector type is deprecated and is no longer used. " - "See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector", +MARKED_FIXTURE = PytestRemovedIn9Warning( + "Marks applied to fixtures have no effect\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" ) # You want to make some `__init__` or function "private". diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/doctest.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/doctest.py index 0784f431b8e..7fff99f37b5 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/doctest.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/doctest.py @@ -1,14 +1,16 @@ +# mypy: allow-untyped-defs """Discover and run doctests in modules and test files.""" + import bdb +from contextlib import contextmanager +import functools import inspect import os +from pathlib import Path import platform import sys import traceback import types -import warnings -from contextlib import contextmanager -from pathlib import Path from typing import Any from typing import Callable from typing import Dict @@ -22,8 +24,8 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union +import warnings -import pytest from _pytest import outcomes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation @@ -32,16 +34,21 @@ from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.config import Config from _pytest.config.argparsing import Parser -from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import fixture +from _pytest.fixtures import TopRequest from _pytest.nodes import Collector +from _pytest.nodes import Item from _pytest.outcomes import OutcomeException +from _pytest.outcomes import skip from _pytest.pathlib import fnmatch_ex -from _pytest.pathlib import import_path +from _pytest.python import Module from _pytest.python_api import approx from _pytest.warning_types import PytestWarning + if TYPE_CHECKING: import doctest + from typing import Self DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -66,26 +73,26 @@ CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None def pytest_addoption(parser: Parser) -> None: parser.addini( "doctest_optionflags", - "option flags for doctests", + "Option flags for doctests", type="args", default=["ELLIPSIS"], ) parser.addini( - "doctest_encoding", "encoding used for doctest files", default="utf-8" + "doctest_encoding", "Encoding used for doctest files", default="utf-8" ) group = parser.getgroup("collect") group.addoption( "--doctest-modules", action="store_true", default=False, - help="run doctests in all .py modules", + help="Run doctests in all .py modules", dest="doctestmodules", ) group.addoption( "--doctest-report", type=str.lower, default="udiff", - help="choose another output format for diffs on doctest failure", + help="Choose another output format for diffs on doctest failure", choices=DOCTEST_REPORT_CHOICES, dest="doctestreport", ) @@ -94,21 +101,21 @@ def pytest_addoption(parser: Parser) -> None: action="append", default=[], metavar="pat", - help="doctests file matching pattern, default: test*.txt", + help="Doctests file matching pattern, default: test*.txt", dest="doctestglob", ) group.addoption( "--doctest-ignore-import-errors", action="store_true", default=False, - help="ignore doctest ImportErrors", + help="Ignore doctest collection errors", dest="doctest_ignore_import_errors", ) group.addoption( "--doctest-continue-on-failure", action="store_true", default=False, - help="for a given doctest, continue to run after the first failure", + help="For a given doctest, continue to run after the first failure", dest="doctest_continue_on_failure", ) @@ -128,11 +135,9 @@ def pytest_collect_file( if config.option.doctestmodules and not any( (_is_setup_py(file_path), _is_main_py(file_path)) ): - mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path) - return mod + return DoctestModule.from_parent(parent, path=file_path) elif _is_doctest(config, file_path, parent): - txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path) - return txt + return DoctestTextfile.from_parent(parent, path=file_path) return None @@ -246,46 +251,51 @@ def _get_runner( ) -class DoctestItem(pytest.Item): +class DoctestItem(Item): def __init__( self, name: str, parent: "Union[DoctestTextfile, DoctestModule]", - runner: Optional["doctest.DocTestRunner"] = None, - dtest: Optional["doctest.DocTest"] = None, + runner: "doctest.DocTestRunner", + dtest: "doctest.DocTest", ) -> None: super().__init__(name, parent) self.runner = runner self.dtest = dtest + + # Stuff needed for fixture support. self.obj = None - self.fixture_request: Optional[FixtureRequest] = None + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None) + self._fixtureinfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() @classmethod - def from_parent( # type: ignore + def from_parent( # type: ignore[override] cls, parent: "Union[DoctestTextfile, DoctestModule]", *, name: str, runner: "doctest.DocTestRunner", dtest: "doctest.DocTest", - ): + ) -> "Self": # incompatible signature due to imposed limits on subclass """The public named constructor.""" return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) + def _initrequest(self) -> None: + self.funcargs: Dict[str, object] = {} + self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type] + def setup(self) -> None: - if self.dtest is not None: - self.fixture_request = _setup_fixtures(self) - globs = dict(getfixture=self.fixture_request.getfixturevalue) - for name, value in self.fixture_request.getfixturevalue( - "doctest_namespace" - ).items(): - globs[name] = value - self.dtest.globs.update(globs) + self._request._fillfixtures() + globs = dict(getfixture=self._request.getfixturevalue) + for name, value in self._request.getfixturevalue("doctest_namespace").items(): + globs[name] = value + self.dtest.globs.update(globs) def runtest(self) -> None: - assert self.dtest is not None - assert self.runner is not None _check_all_skipped(self.dtest) self._disable_output_capturing_for_darwin() failures: List["doctest.DocTestFailure"] = [] @@ -372,7 +382,6 @@ class DoctestItem(pytest.Item): return ReprFailDoctest(reprlocation_lines) def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: - assert self.dtest is not None return self.path, self.dtest.lineno, "[doctest] %s" % self.name @@ -392,8 +401,8 @@ def _get_flag_lookup() -> Dict[str, int]: ) -def get_optionflags(parent): - optionflags_str = parent.config.getini("doctest_optionflags") +def get_optionflags(config: Config) -> int: + optionflags_str = config.getini("doctest_optionflags") flag_lookup_table = _get_flag_lookup() flag_acc = 0 for flag in optionflags_str: @@ -401,8 +410,8 @@ def get_optionflags(parent): return flag_acc -def _get_continue_on_failure(config): - continue_on_failure = config.getvalue("doctest_continue_on_failure") +def _get_continue_on_failure(config: Config) -> bool: + continue_on_failure: bool = config.getvalue("doctest_continue_on_failure") if continue_on_failure: # We need to turn off this if we use pdb since we should stop at # the first failure. @@ -411,7 +420,7 @@ def _get_continue_on_failure(config): return continue_on_failure -class DoctestTextfile(pytest.Module): +class DoctestTextfile(Module): obj = None def collect(self) -> Iterable[DoctestItem]: @@ -425,7 +434,7 @@ class DoctestTextfile(pytest.Module): name = self.path.name globs = {"__name__": "__main__"} - optionflags = get_optionflags(self) + optionflags = get_optionflags(self.config) runner = _get_runner( verbose=False, @@ -449,7 +458,7 @@ def _check_all_skipped(test: "doctest.DocTest") -> None: all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) if all_skipped: - pytest.skip("all tests skipped by +SKIP option") + skip("all tests skipped by +SKIP option") def _is_mocked(obj: object) -> bool: @@ -477,9 +486,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]: return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) except Exception as e: warnings.warn( - "Got %r when unwrapping %r. This is usually caused " + f"Got {e!r} when unwrapping {func!r}. This is usually caused " "by a violation of Python's object protocol; see e.g. " - "https://github.com/pytest-dev/pytest/issues/5080" % (e, func), + "https://github.com/pytest-dev/pytest/issues/5080", PytestWarning, ) raise @@ -491,7 +500,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]: inspect.unwrap = real_unwrap -class DoctestModule(pytest.Module): +class DoctestModule(Module): def collect(self) -> Iterable[DoctestItem]: import doctest @@ -528,29 +537,43 @@ class DoctestModule(pytest.Module): if _is_mocked(obj): return with _patch_unwrap_mock_aware(): - # Type ignored because this is a private function. super()._find( # type:ignore[misc] tests, obj, name, module, source_lines, globs, seen ) - if self.path.name == "conftest.py": - module = self.config.pluginmanager._importconftest( - self.path, - self.config.getoption("importmode"), - rootpath=self.config.rootpath, - ) - else: - try: - module = import_path(self.path, root=self.config.rootpath) - except ImportError: - if self.config.getvalue("doctest_ignore_import_errors"): - pytest.skip("unable to import module %r" % self.path) - else: - raise + if sys.version_info < (3, 13): + + def _from_module(self, module, object): + """`cached_property` objects are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 + """ + if isinstance(object, functools.cached_property): + object = object.func + + # Type ignored because this is a private function. + return super()._from_module(module, object) # type: ignore[misc] + + else: # pragma: no cover + pass + + try: + module = self.obj + except Collector.CollectError: + if self.config.getvalue("doctest_ignore_import_errors"): + skip("unable to import module %r" % self.path) + else: + raise + + # While doctests currently don't support fixtures directly, we still + # need to pick up autouse fixtures. + self.session._fixturemanager.parsefactories(self) + # Uses internal doctest module parsing mechanism. finder = MockAwareDocTestFinder() - optionflags = get_optionflags(self) + optionflags = get_optionflags(self.config) runner = _get_runner( verbose=False, optionflags=optionflags, @@ -565,22 +588,6 @@ class DoctestModule(pytest.Module): ) -def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: - """Used by DoctestTextfile and DoctestItem to setup fixture information.""" - - def func() -> None: - pass - - doctest_item.funcargs = {} # type: ignore[attr-defined] - fm = doctest_item.session._fixturemanager - doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] - node=doctest_item, func=func, cls=None, funcargs=False - ) - fixture_request = FixtureRequest(doctest_item, _ispytest=True) - fixture_request._fillfixtures() - return fixture_request - - def _init_checker_class() -> Type["doctest.OutputChecker"]: import doctest import re @@ -656,7 +663,7 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]: precision = 0 if fraction is None else len(fraction) if exponent is not None: precision -= int(exponent) - if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + if float(w.group()) == approx(float(g.group()), abs=10**-precision): # They're close enough. Replace the text we actually # got with the text we want, so that it will match when we # check the string literally. @@ -727,8 +734,19 @@ def _get_report_choice(key: str) -> int: }[key] -@pytest.fixture(scope="session") +@fixture(scope="session") def doctest_namespace() -> Dict[str, Any]: """Fixture that returns a :py:class:`dict` that will be injected into the - namespace of doctests.""" + namespace of doctests. + + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + """ return dict() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/faulthandler.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/faulthandler.py index aaee307ff2c..083bcb83739 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/faulthandler.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/faulthandler.py @@ -1,24 +1,22 @@ -import io import os import sys from typing import Generator -from typing import TextIO -import pytest from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.stash import StashKey +import pytest -fault_handler_stderr_key = StashKey[TextIO]() -fault_handler_originally_enabled_key = StashKey[bool]() +fault_handler_original_stderr_fd_key = StashKey[int]() +fault_handler_stderr_fd_key = StashKey[int]() def pytest_addoption(parser: Parser) -> None: help = ( "Dump the traceback of all threads if a test takes " - "more than TIMEOUT seconds to finish." + "more than TIMEOUT seconds to finish" ) parser.addini("faulthandler_timeout", help, default=0.0) @@ -26,10 +24,16 @@ def pytest_addoption(parser: Parser) -> None: def pytest_configure(config: Config) -> None: import faulthandler - stderr_fd_copy = os.dup(get_stderr_fileno()) - config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w") - config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled() - faulthandler.enable(file=config.stash[fault_handler_stderr_key]) + # at teardown we want to restore the original faulthandler fileno + # but faulthandler has no api to return the original fileno + # so here we stash the stderr fileno to be used at teardown + # sys.stderr and sys.__stderr__ may be closed or patched during the session + # so we can't rely on their values being good at that point (#11572). + stderr_fileno = get_stderr_fileno() + if faulthandler.is_enabled(): + config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno + config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno) + faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key]) def pytest_unconfigure(config: Config) -> None: @@ -37,12 +41,13 @@ def pytest_unconfigure(config: Config) -> None: faulthandler.disable() # Close the dup file installed during pytest_configure. - if fault_handler_stderr_key in config.stash: - config.stash[fault_handler_stderr_key].close() - del config.stash[fault_handler_stderr_key] - if config.stash.get(fault_handler_originally_enabled_key, False): - # Re-enable the faulthandler if it was originally enabled. - faulthandler.enable(file=get_stderr_fileno()) + if fault_handler_stderr_fd_key in config.stash: + os.close(config.stash[fault_handler_stderr_fd_key]) + del config.stash[fault_handler_stderr_fd_key] + # Re-enable the faulthandler if it was originally enabled. + if fault_handler_original_stderr_fd_key in config.stash: + faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key]) + del config.stash[fault_handler_original_stderr_fd_key] def get_stderr_fileno() -> int: @@ -53,7 +58,7 @@ def get_stderr_fileno() -> int: if fileno == -1: raise AttributeError() return fileno - except (AttributeError, io.UnsupportedOperation): + except (AttributeError, ValueError): # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors # This is potentially dangerous, but the best we can do. @@ -64,20 +69,20 @@ def get_timeout_config_value(config: Config) -> float: return float(config.getini("faulthandler_timeout") or 0.0) -@pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: timeout = get_timeout_config_value(item.config) - stderr = item.config.stash[fault_handler_stderr_key] - if timeout > 0 and stderr is not None: + if timeout > 0: import faulthandler + stderr = item.config.stash[fault_handler_stderr_fd_key] faulthandler.dump_traceback_later(timeout, file=stderr) try: - yield + return (yield) finally: faulthandler.cancel_dump_traceback_later() else: - yield + return (yield) @pytest.hookimpl(tryfirst=True) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/fixtures.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/fixtures.py index fddff931c51..7fd63f937c1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/fixtures.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/fixtures.py @@ -1,45 +1,46 @@ +# mypy: allow-untyped-defs +import abc +from collections import defaultdict +from collections import deque +import dataclasses import functools import inspect import os -import sys -import warnings -from collections import defaultdict -from collections import deque -from contextlib import suppress from pathlib import Path -from types import TracebackType +import sys +from typing import AbstractSet from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import Final +from typing import final from typing import Generator from typing import Generic from typing import Iterable from typing import Iterator from typing import List from typing import MutableMapping +from typing import NoReturn from typing import Optional from typing import overload from typing import Sequence from typing import Set from typing import Tuple -from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - -import attr +import warnings import _pytest from _pytest import nodes from _pytest._code import getfslineno +from _pytest._code import Source from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames @@ -47,30 +48,35 @@ from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET +from _pytest.compat import NotSetType from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config +from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest -from _pytest.deprecated import FILLFUNCARGS +from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.mark.structures import MarkDecorator from _pytest.outcomes import fail +from _pytest.outcomes import skip from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath +from _pytest.scope import _ScopeName from _pytest.scope import HIGH_SCOPES from _pytest.scope import Scope -from _pytest.stash import StashKey + + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup if TYPE_CHECKING: from typing import Deque - from typing import NoReturn - from _pytest.scope import _ScopeName from _pytest.main import Session from _pytest.python import CallSpec2 from _pytest.python import Function @@ -98,13 +104,13 @@ _FixtureCachedResult = Union[ None, # Cache key. object, - # Exc info if raised. - Tuple[Type[BaseException], BaseException, TracebackType], + # Exception if raised. + BaseException, ], ] -@attr.s(frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class PseudoFixtureDef(Generic[FixtureValue]): cached_result: "_FixtureCachedResult[FixtureValue]" _scope: Scope @@ -114,28 +120,25 @@ def pytest_sessionstart(session: "Session") -> None: session._fixturemanager = FixtureManager(session) -def get_scope_package(node, fixturedef: "FixtureDef[object]"): - import pytest +def get_scope_package( + node: nodes.Item, + fixturedef: "FixtureDef[object]", +) -> Optional[nodes.Node]: + from _pytest.python import Package - cls = pytest.Package - current = node - fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") - while current and ( - type(current) is not cls or fixture_package_name != current.nodeid - ): - current = current.parent - if current is None: - return node.session - return current + for parent in node.iter_parents(): + if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid: + return parent + return node.session -def get_scope_node( - node: nodes.Node, scope: Scope -) -> Optional[Union[nodes.Item, nodes.Collector]]: +def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: import _pytest.python if scope is Scope.Function: - return node.getparent(nodes.Item) + # Type ignored because this is actually safe, see: + # https://github.com/python/mypy/issues/4717 + return node.getparent(nodes.Item) # type: ignore[type-abstract] elif scope is Scope.Class: return node.getparent(_pytest.python.Class) elif scope is Scope.Module: @@ -148,126 +151,52 @@ def get_scope_node( assert_never(scope) -# Used for storing artificial fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]() - - -def add_funcarg_pseudo_fixture_def( - collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" -) -> None: - # This function will transform all collected calls to functions - # if they use direct funcargs (i.e. direct parametrization) - # because we want later test execution to be able to rely on - # an existing FixtureDef structure for all arguments. - # XXX we can probably avoid this algorithm if we modify CallSpec2 - # to directly care for creating the fixturedefs within its methods. - if not metafunc._calls[0].funcargs: - # This function call does not have direct parametrization. - return - # Collect funcargs of all callspecs into a list of values. - arg2params: Dict[str, List[object]] = {} - arg2scope: Dict[str, Scope] = {} - for callspec in metafunc._calls: - for argname, argvalue in callspec.funcargs.items(): - assert argname not in callspec.params - callspec.params[argname] = argvalue - arg2params_list = arg2params.setdefault(argname, []) - callspec.indices[argname] = len(arg2params_list) - arg2params_list.append(argvalue) - if argname not in arg2scope: - scope = callspec._arg2scope.get(argname, Scope.Function) - arg2scope[argname] = scope - callspec.funcargs.clear() - - # Register artificial FixtureDef's so that later at test execution - # time we can rely on a proper FixtureDef to exist for fixture setup. - arg2fixturedefs = metafunc._arg2fixturedefs - for argname, valuelist in arg2params.items(): - # If we have a scope that is higher than function, we need - # to make sure we only ever create an according fixturedef on - # a per-scope basis. We thus store and cache the fixturedef on the - # node related to the scope. - scope = arg2scope[argname] - node = None - if scope is not Scope.Function: - node = get_scope_node(collector, scope) - if node is None: - assert scope is Scope.Class and isinstance( - collector, _pytest.python.Module - ) - # Use module-level collector for class-scope (for now). - node = collector - if node is None: - name2pseudofixturedef = None - else: - default: Dict[str, FixtureDef[Any]] = {} - name2pseudofixturedef = node.stash.setdefault( - name2pseudofixturedef_key, default - ) - if name2pseudofixturedef is not None and argname in name2pseudofixturedef: - arg2fixturedefs[argname] = [name2pseudofixturedef[argname]] - else: - fixturedef = FixtureDef( - fixturemanager=fixturemanager, - baseid="", - argname=argname, - func=get_direct_param_fixture_func, - scope=arg2scope[argname], - params=valuelist, - unittest=False, - ids=None, - ) - arg2fixturedefs[argname] = [fixturedef] - if name2pseudofixturedef is not None: - name2pseudofixturedef[argname] = fixturedef - - def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" - try: - fixturemarker: Optional[FixtureFunctionMarker] = getattr( - obj, "_pytestfixturefunction", None - ) - except TEST_OUTCOME: - # some objects raise errors like request (from flask import request) - # we don't expect them to be fixture functions - return None - return fixturemarker + return cast( + Optional[FixtureFunctionMarker], + safe_getattr(obj, "_pytestfixturefunction", None), + ) -# Parametrized fixture key, helper alias for code below. -_Key = Tuple[object, ...] +@dataclasses.dataclass(frozen=True) +class FixtureArgKey: + argname: str + param_index: int + scoped_item_path: Optional[Path] + item_cls: Optional[type] -def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]: +def get_parametrized_fixture_keys( + item: nodes.Item, scope: Scope +) -> Iterator[FixtureArgKey]: """Return list of keys for all parametrized arguments which match the specified scope.""" assert scope is not Scope.Function try: - callspec = item.callspec # type: ignore[attr-defined] + callspec: CallSpec2 = item.callspec # type: ignore[attr-defined] except AttributeError: - pass - else: - cs: CallSpec2 = callspec - # cs.indices.items() is random order of argnames. Need to - # sort this so that different calls to - # get_parametrized_fixture_keys will be deterministic. - for argname, param_index in sorted(cs.indices.items()): - if cs._arg2scope[argname] != scope: - continue - if scope is Scope.Session: - key: _Key = (argname, param_index) - elif scope is Scope.Package: - key = (argname, param_index, item.path.parent) - elif scope is Scope.Module: - key = (argname, param_index, item.path) - elif scope is Scope.Class: - item_cls = item.cls # type: ignore[attr-defined] - key = (argname, param_index, item.path, item_cls) - else: - assert_never(scope) - yield key + return + for argname in callspec.indices: + if callspec._arg2scope[argname] != scope: + continue + + item_cls = None + if scope is Scope.Session: + scoped_item_path = None + elif scope is Scope.Package: + scoped_item_path = item.path + elif scope is Scope.Module: + scoped_item_path = item.path + elif scope is Scope.Class: + scoped_item_path = item.path + item_cls = item.cls # type: ignore[attr-defined] + else: + assert_never(scope) + + param_index = callspec.indices[argname] + yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) # Algorithm for sorting on a per-parametrized resource setup basis. @@ -277,19 +206,17 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {} - items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {} + argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {} + items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {} for scope in HIGH_SCOPES: - d: Dict[nodes.Item, Dict[_Key, None]] = {} - argkeys_cache[scope] = d - item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque) - items_by_argkey[scope] = item_d + scoped_argkeys_cache = argkeys_cache[scope] = {} + scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque) for item in items: keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None) if keys: - d[item] = keys + scoped_argkeys_cache[item] = keys for key in keys: - item_d[key].append(item) + scoped_items_by_argkey[key].append(item) items_dict = dict.fromkeys(items, None) return list( reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session) @@ -298,8 +225,8 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: def fix_cache_order( item: nodes.Item, - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]], - items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]], + argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], + items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], ) -> None: for scope in HIGH_SCOPES: for key in argkeys_cache[scope].get(item, []): @@ -308,13 +235,13 @@ def fix_cache_order( def reorder_items_atscope( items: Dict[nodes.Item, None], - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]], - items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]], + argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], + items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], scope: Scope, ) -> Dict[nodes.Item, None]: if scope is Scope.Function or len(items) < 3: return items - ignore: Set[Optional[_Key]] = set() + ignore: Set[Optional[FixtureArgKey]] = set() items_deque = deque(items) items_done: Dict[nodes.Item, None] = {} scoped_items_by_argkey = items_by_argkey[scope] @@ -352,54 +279,35 @@ def reorder_items_atscope( return items_done -def _fillfuncargs(function: "Function") -> None: - """Fill missing fixtures for a test function, old public API (deprecated).""" - warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2) - _fill_fixtures_impl(function) - - -def fillfixtures(function: "Function") -> None: - """Fill missing fixtures for a test function (deprecated).""" - warnings.warn( - FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2 - ) - _fill_fixtures_impl(function) - - -def _fill_fixtures_impl(function: "Function") -> None: - """Internal implementation to fill fixtures on the given function object.""" - try: - request = function._request - except AttributeError: - # XXX this special code path is only expected to execute - # with the oejskit plugin. It uses classes with funcargs - # and we thus have to work a bit to allow this. - fm = function.session._fixturemanager - assert function.parent is not None - fi = fm.getfixtureinfo(function.parent, function.obj, None) - function._fixtureinfo = fi - request = function._request = FixtureRequest(function, _ispytest=True) - fm.session._setupstate.setup(function) - request._fillfixtures() - # Prune out funcargs for jstests. - function.funcargs = {name: function.funcargs[name] for name in fi.argnames} - else: - request._fillfixtures() +@dataclasses.dataclass(frozen=True) +class FuncFixtureInfo: + """Fixture-related information for a fixture-requesting item (e.g. test + function). + This is used to examine the fixtures which an item requests statically + (known during collection). This includes autouse fixtures, fixtures + requested by the `usefixtures` marker, fixtures requested in the function + parameters, and the transitive closure of these. -def get_direct_param_fixture_func(request): - return request.param + An item may also request fixtures dynamically (using `request.getfixturevalue`); + these are not reflected here. + """ + __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") -@attr.s(slots=True, auto_attribs=True) -class FuncFixtureInfo: - # Original function argument names. + # Fixture names that the item requests directly by function parameters. argnames: Tuple[str, ...] - # Argnames that function immediately requires. These include argnames + - # fixture names specified via usefixtures and via autouse=True in fixture - # definitions. + # Fixture names that the item immediately requires. These include + # argnames + fixture names specified via usefixtures and via autouse=True in + # fixture definitions. initialnames: Tuple[str, ...] + # The transitive closure of the fixture names that the item requires. + # Note: can't include dynamic dependencies (`request.getfixturevalue` calls). names_closure: List[str] + # A map from a fixture name in the transitive closure to the FixtureDefs + # matching the name which are applicable to this function. + # There may be multiple overriding fixtures with the same name. The + # sequence is ordered from furthest to closes to the function. name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] def prune_dependency_tree(self) -> None: @@ -417,7 +325,7 @@ class FuncFixtureInfo: working_set = set(self.initialnames) while working_set: argname = working_set.pop() - # Argname may be smth not included in the original names_closure, + # Argname may be something not included in the original names_closure, # in which case we ignore it. This currently happens with pseudo # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. # So they introduce the new dependency 'request' which might have @@ -430,66 +338,83 @@ class FuncFixtureInfo: self.names_closure[:] = sorted(closure, key=self.names_closure.index) -class FixtureRequest: - """A request for a fixture from a test or fixture function. +class FixtureRequest(abc.ABC): + """The type of the ``request`` fixture. - A request object gives access to the requesting test context and has - an optional ``param`` attribute in case the fixture is parametrized - indirectly. + A request object gives access to the requesting test context and has a + ``param`` attribute in case the fixture is parametrized. """ - def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: + def __init__( + self, + pyfuncitem: "Function", + fixturename: Optional[str], + arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]], + fixture_defs: Dict[str, "FixtureDef[Any]"], + *, + _ispytest: bool = False, + ) -> None: check_ispytest(_ispytest) - self._pyfuncitem = pyfuncitem #: Fixture for which this request is being performed. - self.fixturename: Optional[str] = None - self._scope = Scope.Function - self._fixture_defs: Dict[str, FixtureDef[Any]] = {} - fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() - self._arg2index: Dict[str, int] = {} - self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager + self.fixturename: Final = fixturename + self._pyfuncitem: Final = pyfuncitem + # The FixtureDefs for each fixture name requested by this item. + # Starts from the statically-known fixturedefs resolved during + # collection. Dynamically requested fixtures (using + # `request.getfixturevalue("foo")`) are added dynamically. + self._arg2fixturedefs: Final = arg2fixturedefs + # The evaluated argnames so far, mapping to the FixtureDef they resolved + # to. + self._fixture_defs: Final = fixture_defs + # Notes on the type of `param`: + # -`request.param` is only defined in parametrized fixtures, and will raise + # AttributeError otherwise. Python typing has no notion of "undefined", so + # this cannot be reflected in the type. + # - Technically `param` is only (possibly) defined on SubRequest, not + # FixtureRequest, but the typing of that is still in flux so this cheats. + # - In the future we might consider using a generic for the param type, but + # for now just using Any. + self.param: Any + + @property + def _fixturemanager(self) -> "FixtureManager": + return self._pyfuncitem.session._fixturemanager + + @property + @abc.abstractmethod + def _scope(self) -> Scope: + raise NotImplementedError() @property - def scope(self) -> "_ScopeName": + def scope(self) -> _ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value + @abc.abstractmethod + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, + ) -> None: + raise NotImplementedError() + @property def fixturenames(self) -> List[str]: """Names of all active fixtures in this request.""" - result = list(self._pyfuncitem._fixtureinfo.names_closure) + result = list(self._pyfuncitem.fixturenames) result.extend(set(self._fixture_defs).difference(result)) return result @property + @abc.abstractmethod def node(self): """Underlying collection node (depends on current request scope).""" - return self._getscopeitem(self._scope) - - def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": - fixturedefs = self._arg2fixturedefs.get(argname, None) - if fixturedefs is None: - # We arrive here because of a dynamic call to - # getfixturevalue(argname) usage which was naturally - # not known at parsing/collection time. - assert self._pyfuncitem.parent is not None - parentid = self._pyfuncitem.parent.nodeid - fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - # TODO: Fix this type ignore. Either add assert or adjust types. - # Can this be None here? - self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] - # fixturedefs list is immutable so we maintain a decreasing index. - index = self._arg2index.get(argname, 0) - 1 - if fixturedefs is None or (-index > len(fixturedefs)): - raise FixtureLookupError(argname, self) - self._arg2index[argname] = index - return fixturedefs[index] + raise NotImplementedError() @property def config(self) -> Config: """The pytest config object associated with this request.""" - return self._pyfuncitem.config # type: ignore[no-any-return] + return self._pyfuncitem.config @property def function(self): @@ -512,26 +437,25 @@ class FixtureRequest: @property def instance(self): """Instance (can be None) on which test function was collected.""" - # unittest support hack, see _pytest.unittest.TestCaseFunction. - try: - return self._pyfuncitem._testcase - except AttributeError: - function = getattr(self, "function", None) - return getattr(function, "__self__", None) + if self.scope != "function": + return None + return getattr(self._pyfuncitem, "instance", None) @property def module(self): """Python module object where the test function was collected.""" if self.scope not in ("function", "class", "module"): raise AttributeError(f"module not available in {self.scope}-scoped context") - return self._pyfuncitem.getparent(_pytest.python.Module).obj + mod = self._pyfuncitem.getparent(_pytest.python.Module) + assert mod is not None + return mod.obj @property def path(self) -> Path: + """Path where the test function was collected.""" if self.scope not in ("function", "class", "module", "package"): raise AttributeError(f"path not available in {self.scope}-scoped context") - # TODO: Remove ignore once _pyfuncitem is properly typed. - return self._pyfuncitem.path # type: ignore + return self._pyfuncitem.path @property def keywords(self) -> MutableMapping[str, Any]: @@ -542,17 +466,13 @@ class FixtureRequest: @property def session(self) -> "Session": """Pytest session object.""" - return self._pyfuncitem.session # type: ignore[no-any-return] + return self._pyfuncitem.session + @abc.abstractmethod def addfinalizer(self, finalizer: Callable[[], object]) -> None: - """Add finalizer/teardown function to be called after the last test - within the requesting test context finished execution.""" - # XXX usually this method is shadowed by fixturedef specific ones. - self._addfinalizer(finalizer, scope=self.scope) - - def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: - node = self._getscopeitem(scope) - node.addfinalizer(finalizer) + """Add finalizer/teardown function to be called without arguments after + the last test within the requesting test context finished execution.""" + raise NotImplementedError() def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. @@ -561,21 +481,17 @@ class FixtureRequest: on all function invocations. :param marker: - A :class:`pytest.MarkDecorator` object created by a call - to ``pytest.mark.NAME(...)``. + An object created by a call to ``pytest.mark.NAME(...)``. """ self.node.add_marker(marker) - def raiseerror(self, msg: Optional[str]) -> "NoReturn": - """Raise a FixtureLookupError with the given message.""" - raise self._fixturemanager.FixtureLookupError(None, self, msg) + def raiseerror(self, msg: Optional[str]) -> NoReturn: + """Raise a FixtureLookupError exception. - def _fillfixtures(self) -> None: - item = self._pyfuncitem - fixturenames = getattr(item, "fixturenames", self.fixturenames) - for argname in fixturenames: - if argname not in item.funcargs: - item.funcargs[argname] = self.getfixturevalue(argname) + :param msg: + An optional custom error message. + """ + raise FixtureLookupError(None, self, msg) def getfixturevalue(self, argname: str) -> Any: """Dynamically run a named fixture function. @@ -585,186 +501,199 @@ class FixtureRequest: setup time, you may use this function to retrieve it inside a fixture or test function body. + This method can be used during the test setup phase or the test run + phase, but during the test teardown phase a fixture's value may not + be available. + + :param argname: + The fixture name. :raises pytest.FixtureLookupError: If the given fixture could not be found. """ + # Note that in addition to the use case described in the docstring, + # getfixturevalue() is also called by pytest itself during item and fixture + # setup to evaluate the fixtures that are requested statically + # (using function parameters, autouse, etc). + fixturedef = self._get_active_fixturedef(argname) - assert fixturedef.cached_result is not None + assert fixturedef.cached_result is not None, ( + f'The fixture value for "{argname}" is not available. ' + "This can happen when the fixture has already been torn down." + ) return fixturedef.cached_result[0] - def _get_active_fixturedef( - self, argname: str - ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: - try: - return self._fixture_defs[argname] - except KeyError: - try: - fixturedef = self._getnextfixturedef(argname) - except FixtureLookupError: - if argname == "request": - cached_result = (self, [0], None) - return PseudoFixtureDef(cached_result, Scope.Function) - raise - # Remove indent to prevent the python3 exception - # from leaking into the call. - self._compute_fixture_value(fixturedef) - self._fixture_defs[argname] = fixturedef - return fixturedef + def _iter_chain(self) -> Iterator["SubRequest"]: + """Yield all SubRequests in the chain, from self up. - def _get_fixturestack(self) -> List["FixtureDef[Any]"]: + Note: does *not* yield the TopRequest. + """ current = self - values: List[FixtureDef[Any]] = [] while isinstance(current, SubRequest): - values.append(current._fixturedef) # type: ignore[has-type] + yield current current = current._parent_request - values.reverse() - return values - def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: - """Create a SubRequest based on "self" and call the execute method - of the given FixtureDef object. + def _get_active_fixturedef( + self, argname: str + ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: + if argname == "request": + cached_result = (self, [0], None) + return PseudoFixtureDef(cached_result, Scope.Function) - This will force the FixtureDef object to throw away any previous - results and compute a new fixture value, which will be stored into - the FixtureDef object itself. - """ - # prepare a subrequest object before calling fixture function - # (latter managed by fixturedef) - argname = fixturedef.argname - funcitem = self._pyfuncitem - scope = fixturedef._scope + # If we already finished computing a fixture by this name in this item, + # return it. + fixturedef = self._fixture_defs.get(argname) + if fixturedef is not None: + self._check_scope(fixturedef, fixturedef._scope) + return fixturedef + + # Find the appropriate fixturedef. + fixturedefs = self._arg2fixturedefs.get(argname, None) + if fixturedefs is None: + # We arrive here because of a dynamic call to + # getfixturevalue(argname) which was naturally + # not known at parsing/collection time. + fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem) + if fixturedefs is not None: + self._arg2fixturedefs[argname] = fixturedefs + # No fixtures defined with this name. + if fixturedefs is None: + raise FixtureLookupError(argname, self) + # The are no fixtures with this name applicable for the function. + if not fixturedefs: + raise FixtureLookupError(argname, self) + # A fixture may override another fixture with the same name, e.g. a + # fixture in a module can override a fixture in a conftest, a fixture in + # a class can override a fixture in the module, and so on. + # An overriding fixture can request its own name (possibly indirectly); + # in this case it gets the value of the fixture it overrides, one level + # up. + # Check how many `argname`s deep we are, and take the next one. + # `fixturedefs` is sorted from furthest to closest, so use negative + # indexing to go in reverse. + index = -1 + for request in self._iter_chain(): + if request.fixturename == argname: + index -= 1 + # If already consumed all of the available levels, fail. + if -index > len(fixturedefs): + raise FixtureLookupError(argname, self) + fixturedef = fixturedefs[index] + + # Prepare a SubRequest object for calling the fixture. try: - param = funcitem.callspec.getparam(argname) - except (AttributeError, ValueError): + callspec = self._pyfuncitem.callspec + except AttributeError: + callspec = None + if callspec is not None and argname in callspec.params: + param = callspec.params[argname] + param_index = callspec.indices[argname] + # The parametrize invocation scope overrides the fixture's scope. + scope = callspec._arg2scope[argname] + else: param = NOTSET param_index = 0 - has_params = fixturedef.params is not None - fixtures_not_supported = getattr(funcitem, "nofuncargs", False) - if has_params and fixtures_not_supported: - msg = ( - "{name} does not support fixtures, maybe unittest.TestCase subclass?\n" - "Node id: {nodeid}\n" - "Function type: {typename}" - ).format( - name=funcitem.name, - nodeid=funcitem.nodeid, - typename=type(funcitem).__name__, - ) - fail(msg, pytrace=False) - if has_params: - frame = inspect.stack()[3] - frameinfo = inspect.getframeinfo(frame[0]) - source_path = absolutepath(frameinfo.filename) - source_lineno = frameinfo.lineno - try: - source_path_str = str( - source_path.relative_to(funcitem.config.rootpath) - ) - except ValueError: - source_path_str = str(source_path) - msg = ( - "The requested fixture has no parameter defined for test:\n" - " {}\n\n" - "Requested fixture '{}' defined in:\n{}" - "\n\nRequested here:\n{}:{}".format( - funcitem.nodeid, - fixturedef.argname, - getlocation(fixturedef.func, funcitem.config.rootpath), - source_path_str, - source_lineno, - ) - ) - fail(msg, pytrace=False) - else: - param_index = funcitem.callspec.indices[argname] - # If a parametrize invocation set a scope it will override - # the static scope defined with the fixture function. - with suppress(KeyError): - scope = funcitem.callspec._arg2scope[argname] - + scope = fixturedef._scope + self._check_fixturedef_without_param(fixturedef) + self._check_scope(fixturedef, scope) subrequest = SubRequest( self, scope, param, param_index, fixturedef, _ispytest=True ) - # Check if a higher-level scoped fixture accesses a lower level one. - subrequest._check_scope(argname, self._scope, scope) - try: - # Call the fixture function. - fixturedef.execute(request=subrequest) - finally: - self._schedule_finalizers(fixturedef, subrequest) + # Make sure the fixture value is cached, running it if it isn't + fixturedef.execute(request=subrequest) - def _schedule_finalizers( - self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" - ) -> None: - # If fixture function failed it might have registered finalizers. - subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest)) + self._fixture_defs[argname] = fixturedef + return fixturedef + + def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None: + """Check that this request is allowed to execute this fixturedef without + a param.""" + funcitem = self._pyfuncitem + has_params = fixturedef.params is not None + fixtures_not_supported = getattr(funcitem, "nofuncargs", False) + if has_params and fixtures_not_supported: + msg = ( + f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n" + f"Node id: {funcitem.nodeid}\n" + f"Function type: {type(funcitem).__name__}" + ) + fail(msg, pytrace=False) + if has_params: + frame = inspect.stack()[3] + frameinfo = inspect.getframeinfo(frame[0]) + source_path = absolutepath(frameinfo.filename) + source_lineno = frameinfo.lineno + try: + source_path_str = str(source_path.relative_to(funcitem.config.rootpath)) + except ValueError: + source_path_str = str(source_path) + location = getlocation(fixturedef.func, funcitem.config.rootpath) + msg = ( + "The requested fixture has no parameter defined for test:\n" + f" {funcitem.nodeid}\n\n" + f"Requested fixture '{fixturedef.argname}' defined in:\n" + f"{location}\n\n" + f"Requested here:\n" + f"{source_path_str}:{source_lineno}" + ) + fail(msg, pytrace=False) + + def _get_fixturestack(self) -> List["FixtureDef[Any]"]: + values = [request._fixturedef for request in self._iter_chain()] + values.reverse() + return values + + +@final +class TopRequest(FixtureRequest): + """The type of the ``request`` fixture in a test function.""" + + def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: + super().__init__( + fixturename=None, + pyfuncitem=pyfuncitem, + arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(), + fixture_defs={}, + _ispytest=_ispytest, + ) + + @property + def _scope(self) -> Scope: + return Scope.Function def _check_scope( self, - argname: str, - invoking_scope: Scope, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], requested_scope: Scope, ) -> None: - if argname == "request": - return - if invoking_scope > requested_scope: - # Try to report something helpful. - text = "\n".join(self._factorytraceback()) - fail( - f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " - f"fixture {argname} with a {invoking_scope.value} scoped request object, " - f"involved factories:\n{text}", - pytrace=False, - ) + # TopRequest always has function scope so always valid. + pass - def _factorytraceback(self) -> List[str]: - lines = [] - for fixturedef in self._get_fixturestack(): - factory = fixturedef.func - fs, lineno = getfslineno(factory) - if isinstance(fs, Path): - session: Session = self._pyfuncitem.session - p = bestrelpath(session.path, fs) - else: - p = fs - args = _format_args(factory) - lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) - return lines - - def _getscopeitem( - self, scope: Union[Scope, "_ScopeName"] - ) -> Union[nodes.Item, nodes.Collector]: - if isinstance(scope, str): - scope = Scope(scope) - if scope is Scope.Function: - # This might also be a non-function Item despite its attribute name. - node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem - elif scope is Scope.Package: - # FIXME: _fixturedef is not defined on FixtureRequest (this class), - # but on FixtureRequest (a subclass). - node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] - else: - node = get_scope_node(self._pyfuncitem, scope) - if node is None and scope is Scope.Class: - # Fallback to function item itself. - node = self._pyfuncitem - assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format( - scope, self._pyfuncitem - ) - return node + @property + def node(self): + return self._pyfuncitem def __repr__(self) -> str: return "<FixtureRequest for %r>" % (self.node) + def _fillfixtures(self) -> None: + item = self._pyfuncitem + for argname in item.fixturenames: + if argname not in item.funcargs: + item.funcargs[argname] = self.getfixturevalue(argname) + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self.node.addfinalizer(finalizer) + @final class SubRequest(FixtureRequest): - """A sub request for handling getting a fixture from a test function/fixture.""" + """The type of the ``request`` fixture in a fixture function requested + (transitively) by a test function.""" def __init__( self, - request: "FixtureRequest", + request: FixtureRequest, scope: Scope, param: Any, param_index: int, @@ -772,39 +701,76 @@ class SubRequest(FixtureRequest): *, _ispytest: bool = False, ) -> None: - check_ispytest(_ispytest) - self._parent_request = request - self.fixturename = fixturedef.argname + super().__init__( + pyfuncitem=request._pyfuncitem, + fixturename=fixturedef.argname, + fixture_defs=request._fixture_defs, + arg2fixturedefs=request._arg2fixturedefs, + _ispytest=_ispytest, + ) + self._parent_request: Final[FixtureRequest] = request + self._scope_field: Final = scope + self._fixturedef: Final[FixtureDef[object]] = fixturedef if param is not NOTSET: self.param = param - self.param_index = param_index - self._scope = scope - self._fixturedef = fixturedef - self._pyfuncitem = request._pyfuncitem - self._fixture_defs = request._fixture_defs - self._arg2fixturedefs = request._arg2fixturedefs - self._arg2index = request._arg2index - self._fixturemanager = request._fixturemanager + self.param_index: Final = param_index def __repr__(self) -> str: return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>" - def addfinalizer(self, finalizer: Callable[[], object]) -> None: - """Add finalizer/teardown function to be called after the last test - within the requesting test context finished execution.""" - self._fixturedef.addfinalizer(finalizer) + @property + def _scope(self) -> Scope: + return self._scope_field - def _schedule_finalizers( - self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" + @property + def node(self): + scope = self._scope + if scope is Scope.Function: + # This might also be a non-function Item despite its attribute name. + node: Optional[nodes.Node] = self._pyfuncitem + elif scope is Scope.Package: + node = get_scope_package(self._pyfuncitem, self._fixturedef) + else: + node = get_scope_node(self._pyfuncitem, scope) + if node is None and scope is Scope.Class: + # Fallback to function item itself. + node = self._pyfuncitem + assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}' + return node + + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, ) -> None: - # If the executing fixturedef was not explicitly requested in the argument list (via - # getfixturevalue inside the fixture call) then ensure this fixture def will be finished - # first. - if fixturedef.argname not in self.fixturenames: - fixturedef.addfinalizer( - functools.partial(self._fixturedef.finish, request=self) + if isinstance(requested_fixturedef, PseudoFixtureDef): + return + if self._scope > requested_scope: + # Try to report something helpful. + argname = requested_fixturedef.argname + fixture_stack = "\n".join( + self._format_fixturedef_line(fixturedef) + for fixturedef in self._get_fixturestack() + ) + requested_fixture = self._format_fixturedef_line(requested_fixturedef) + fail( + f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " + f"fixture {argname} with a {self._scope.value} scoped request object. " + f"Requesting fixture stack:\n{fixture_stack}\n" + f"Requested fixture:\n{requested_fixture}", + pytrace=False, ) - super()._schedule_finalizers(fixturedef, subrequest) + + def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str: + factory = fixturedef.func + path, lineno = getfslineno(factory) + if isinstance(path, Path): + path = bestrelpath(self._pyfuncitem.session.path, path) + signature = inspect.signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{signature}" + + def addfinalizer(self, finalizer: Callable[[], object]) -> None: + self._fixturedef.addfinalizer(finalizer) @final @@ -847,14 +813,15 @@ class FixtureLookupError(LookupError): if msg is None: fm = self.request._fixturemanager available = set() - parentid = self.request._pyfuncitem.parent.nodeid + parent = self.request._pyfuncitem.parent + assert parent is not None for name, fixturedefs in fm._arg2fixturedefs.items(): - faclist = list(fm._matchfactories(fixturedefs, parentid)) + faclist = list(fm._matchfactories(fixturedefs, parent)) if faclist: available.add(name) if self.argname in available: - msg = " recursive dependency involving fixture '{}' detected".format( - self.argname + msg = ( + f" recursive dependency involving fixture '{self.argname}' detected" ) else: msg = f"fixture '{self.argname}' not found" @@ -898,13 +865,6 @@ class FixtureLookupErrorRepr(TerminalRepr): tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) -def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": - fs, lineno = getfslineno(fixturefunc) - location = f"{fs}:{lineno + 1}" - source = _pytest._code.Source(fixturefunc) - fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) - - def call_fixture_func( fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs ) -> FixtureValue: @@ -934,29 +894,33 @@ def _teardown_yield_fixture(fixturefunc, it) -> None: except StopIteration: pass else: - fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'") + fs, lineno = getfslineno(fixturefunc) + fail( + f"fixture function has more than one 'yield':\n\n" + f"{Source(fixturefunc).indent()}\n" + f"{fs}:{lineno + 1}", + pytrace=False, + ) def _eval_scope_callable( - scope_callable: "Callable[[str, Config], _ScopeName]", + scope_callable: Callable[[str, Config], _ScopeName], fixture_name: str, config: Config, -) -> "_ScopeName": +) -> _ScopeName: try: # Type ignored because there is no typing mechanism to specify # keyword arguments, currently. result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] except Exception as e: raise TypeError( - "Error evaluating {} while defining fixture '{}'.\n" - "Expected a function with the signature (*, fixture_name, config)".format( - scope_callable, fixture_name - ) + f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n" + "Expected a function with the signature (*, fixture_name, config)" ) from e if not isinstance(result, str): fail( - "Expected {} to return a 'str' while defining fixture '{}', but it returned:\n" - "{!r}".format(scope_callable, fixture_name, result), + f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n" + f"{result!r}", pytrace=False, ) return result @@ -964,50 +928,73 @@ def _eval_scope_callable( @final class FixtureDef(Generic[FixtureValue]): - """A container for a factory definition.""" + """A container for a fixture definition. + + Note: At this time, only explicitly documented fields and methods are + considered public stable API. + """ def __init__( self, - fixturemanager: "FixtureManager", + config: Config, baseid: Optional[str], argname: str, func: "_FixtureFunc[FixtureValue]", - scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None], + scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None], params: Optional[Sequence[object]], - unittest: bool = False, ids: Optional[ - Union[ - Tuple[Union[None, str, float, int, bool], ...], - Callable[[Any], Optional[object]], - ] + Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] ] = None, + *, + _ispytest: bool = False, ) -> None: - self._fixturemanager = fixturemanager - self.baseid = baseid or "" - self.has_location = baseid is not None - self.func = func - self.argname = argname + check_ispytest(_ispytest) + # The "base" node ID for the fixture. + # + # This is a node ID prefix. A fixture is only available to a node (e.g. + # a `Function` item) if the fixture's baseid is a nodeid of a parent of + # node. + # + # For a fixture found in a Collector's object (e.g. a `Module`s module, + # a `Class`'s class), the baseid is the Collector's nodeid. + # + # For a fixture found in a conftest plugin, the baseid is the conftest's + # directory path relative to the rootdir. + # + # For other plugins, the baseid is the empty string (always matches). + self.baseid: Final = baseid or "" + # Whether the fixture was found from a node or a conftest in the + # collection tree. Will be false for fixtures defined in non-conftest + # plugins. + self.has_location: Final = baseid is not None + # The fixture factory function. + self.func: Final = func + # The name by which the fixture may be requested. + self.argname: Final = argname if scope is None: scope = Scope.Function elif callable(scope): - scope = _eval_scope_callable(scope, argname, fixturemanager.config) - + scope = _eval_scope_callable(scope, argname, config) if isinstance(scope, str): scope = Scope.from_user( scope, descr=f"Fixture '{func.__name__}'", where=baseid ) - self._scope = scope - self.params: Optional[Sequence[object]] = params - self.argnames: Tuple[str, ...] = getfuncargnames( - func, name=argname, is_method=unittest - ) - self.unittest = unittest - self.ids = ids + self._scope: Final = scope + # If the fixture is directly parametrized, the parameter values. + self.params: Final = params + # If the fixture is directly parametrized, a tuple of explicit IDs to + # assign to the parameter values, or a callable to generate an ID given + # a parameter value. + self.ids: Final = ids + # The names requested by the fixtures. + self.argnames: Final = getfuncargnames(func, name=argname) + # If the fixture was executed, the current value of the fixture. + # Can change if the fixture is executed with different parameters. self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None - self._finalizers: List[Callable[[], object]] = [] + self._finalizers: Final[List[Callable[[], object]]] = [] @property - def scope(self) -> "_ScopeName": + def scope(self) -> _ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value @@ -1015,47 +1002,55 @@ class FixtureDef(Generic[FixtureValue]): self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - exc = None - try: - while self._finalizers: - try: - func = self._finalizers.pop() - func() - except BaseException as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc - finally: - hook = self._fixturemanager.session.gethookproxy(request.node.path) - hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) - # Even if finalization fails, we invalidate the cached fixture - # value and remove all finalizers because they may be bound methods - # which will keep instances alive. - self.cached_result = None - self._finalizers = [] + exceptions: List[BaseException] = [] + while self._finalizers: + fin = self._finalizers.pop() + try: + fin() + except BaseException as e: + exceptions.append(e) + node = request.node + node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request) + # Even if finalization fails, we invalidate the cached fixture + # value and remove all finalizers because they may be bound methods + # which will keep instances alive. + self.cached_result = None + self._finalizers.clear() + if len(exceptions) == 1: + raise exceptions[0] + elif len(exceptions) > 1: + msg = f'errors while tearing down fixture "{self.argname}" of {node}' + raise BaseExceptionGroup(msg, exceptions[::-1]) def execute(self, request: SubRequest) -> FixtureValue: - # Get required arguments and register our own finish() - # with their finalization. + """Return the value of this fixture, executing it if not cached.""" + # Ensure that the dependent fixtures requested by this fixture are loaded. + # This needs to be done before checking if we have a cached value, since + # if a dependent fixture has their cache invalidated, e.g. due to + # parametrization, they finalize themselves and fixtures depending on it + # (which will likely include this fixture) setting `self.cached_result = None`. + # See #4871 + requested_fixtures_that_should_finalize_us = [] for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) - if argname != "request": - # PseudoFixtureDef is only for "request". - assert isinstance(fixturedef, FixtureDef) - fixturedef.addfinalizer(functools.partial(self.finish, request=request)) - + # Saves requested fixtures in a list so we later can add our finalizer + # to them, ensuring that if a requested fixture gets torn down we get torn + # down first. This is generally handled by SetupState, but still currently + # needed when this fixture is not parametrized but depends on a parametrized + # fixture. + if not isinstance(fixturedef, PseudoFixtureDef): + requested_fixtures_that_should_finalize_us.append(fixturedef) + + # Check for (and return) cached value/exception. my_cache_key = self.cache_key(request) if self.cached_result is not None: + cache_key = self.cached_result[1] # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497). - cache_key = self.cached_result[1] if my_cache_key is cache_key: if self.cached_result[2] is not None: - _, val, tb = self.cached_result[2] - raise val.with_traceback(tb) + exc = self.cached_result[2] + raise exc else: result = self.cached_result[0] return result @@ -1064,43 +1059,52 @@ class FixtureDef(Generic[FixtureValue]): self.finish(request) assert self.cached_result is None - hook = self._fixturemanager.session.gethookproxy(request.node.path) - result = hook.pytest_fixture_setup(fixturedef=self, request=request) + # Add finalizer to requested fixtures we saved previously. + # We make sure to do this after checking for cached value to avoid + # adding our finalizer multiple times. (#12135) + finalizer = functools.partial(self.finish, request=request) + for parent_fixture in requested_fixtures_that_should_finalize_us: + parent_fixture.addfinalizer(finalizer) + + ihook = request.node.ihook + try: + # Setup the fixture, run the code in it, and cache the value + # in self.cached_result + result = ihook.pytest_fixture_setup(fixturedef=self, request=request) + finally: + # schedule our finalizer, even if the setup failed + request.node.addfinalizer(finalizer) + return result def cache_key(self, request: SubRequest) -> object: - return request.param_index if not hasattr(request, "param") else request.param + return getattr(request, "param", None) def __repr__(self) -> str: - return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format( - self.argname, self.scope, self.baseid - ) + return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>" def resolve_fixture_function( fixturedef: FixtureDef[FixtureValue], request: FixtureRequest ) -> "_FixtureFunc[FixtureValue]": """Get the actual callable that can be called to obtain the fixture - value, dealing with unittest-specific instances and bound methods.""" + value.""" fixturefunc = fixturedef.func - if fixturedef.unittest: - if request.instance is not None: - # Bind the unbound method to the TestCase instance. - fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] - else: - # The fixture function needs to be bound to the actual - # request.instance so that code working with "fixturedef" behaves - # as expected. - if request.instance is not None: - # Handle the case where fixture is defined not in a test class, but some other class - # (for example a plugin class with a fixture), see #2270. - if hasattr(fixturefunc, "__self__") and not isinstance( - request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] - ): - return fixturefunc - fixturefunc = getimfunc(fixturedef.func) - if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] + # The fixture function needs to be bound to the actual + # request.instance so that code working with "fixturedef" behaves + # as expected. + instance = request.instance + if instance is not None: + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. + if hasattr(fixturefunc, "__self__") and not isinstance( + instance, + fixturefunc.__self__.__class__, + ): + return fixturefunc + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(instance) return fixturefunc @@ -1110,63 +1114,37 @@ def pytest_fixture_setup( """Execution of fixture setup.""" kwargs = {} for argname in fixturedef.argnames: - fixdef = request._get_active_fixturedef(argname) - assert fixdef.cached_result is not None - result, arg_cache_key, exc = fixdef.cached_result - request._check_scope(argname, request._scope, fixdef._scope) - kwargs[argname] = result + kwargs[argname] = request.getfixturevalue(argname) fixturefunc = resolve_fixture_function(fixturedef, request) my_cache_key = fixturedef.cache_key(request) try: result = call_fixture_func(fixturefunc, request, kwargs) - except TEST_OUTCOME: - exc_info = sys.exc_info() - assert exc_info[0] is not None - fixturedef.cached_result = (None, my_cache_key, exc_info) + except TEST_OUTCOME as e: + if isinstance(e, skip.Exception): + # The test requested a fixture which caused a skip. + # Don't show the fixture as the skip location, as then the user + # wouldn't know which test skipped. + e._use_item_location = True + fixturedef.cached_result = (None, my_cache_key, e) raise fixturedef.cached_result = (result, my_cache_key, None) return result -def _ensure_immutable_ids( - ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] - ], -) -> Optional[ - Union[ - Tuple[Union[None, str, float, int, bool], ...], - Callable[[Any], Optional[object]], - ] -]: - if ids is None: - return None - if callable(ids): - return ids - return tuple(ids) - - -def _params_converter( - params: Optional[Iterable[object]], -) -> Optional[Tuple[object, ...]]: - return tuple(params) if params is not None else None - - def wrap_function_to_error_out_if_called_directly( function: FixtureFunction, fixture_marker: "FixtureFunctionMarker", ) -> FixtureFunction: """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function.""" + name = fixture_marker.name or function.__name__ message = ( - 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' + f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' "but are created automatically when test functions request them as parameters.\n" "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n" "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." - ).format(name=fixture_marker.name or function.__name__) + ) @functools.wraps(function) def result(*args, **kwargs): @@ -1180,38 +1158,40 @@ def wrap_function_to_error_out_if_called_directly( @final -@attr.s(frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class FixtureFunctionMarker: scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" - params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter) + params: Optional[Tuple[object, ...]] autouse: bool = False - ids: Union[ - Tuple[Union[None, str, float, int, bool], ...], - Callable[[Any], Optional[object]], - ] = attr.ib( - default=None, - converter=_ensure_immutable_ids, - ) + ids: Optional[ + Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] + ] = None name: Optional[str] = None + _ispytest: dataclasses.InitVar[bool] = False + + def __post_init__(self, _ispytest: bool) -> None: + check_ispytest(_ispytest) + def __call__(self, function: FixtureFunction) -> FixtureFunction: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") if getattr(function, "_pytestfixturefunction", False): raise ValueError( - "fixture is being applied more than once to the same function" + f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}" ) + if hasattr(function, "pytestmark"): + warnings.warn(MARKED_FIXTURE, stacklevel=2) + function = wrap_function_to_error_out_if_called_directly(function, self) name = self.name or function.__name__ if name == "request": location = getlocation(function) fail( - "'request' is a reserved word for fixtures, use another name:\n {}".format( - location - ), + f"'request' is a reserved word for fixtures, use another name:\n {location}", pytrace=False, ) @@ -1228,14 +1208,10 @@ def fixture( params: Optional[Iterable[object]] = ..., autouse: bool = ..., ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = ..., -) -> FixtureFunction: - ... +) -> FixtureFunction: ... @overload @@ -1246,14 +1222,10 @@ def fixture( params: Optional[Iterable[object]] = ..., autouse: bool = ..., ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = ..., name: Optional[str] = None, -) -> FixtureFunctionMarker: - ... +) -> FixtureFunctionMarker: ... def fixture( @@ -1263,10 +1235,7 @@ def fixture( params: Optional[Iterable[object]] = None, autouse: bool = False, ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] + Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] ] = None, name: Optional[str] = None, ) -> Union[FixtureFunctionMarker, FixtureFunction]: @@ -1308,7 +1277,7 @@ def fixture( the fixture. :param ids: - List of string ids each corresponding to the params so that they are + Sequence of ids each corresponding to the params so that they are part of the test id. If no ids are provided they will be generated automatically from the params. @@ -1322,10 +1291,11 @@ def fixture( """ fixture_marker = FixtureFunctionMarker( scope=scope, - params=params, + params=tuple(params) if params is not None else None, autouse=autouse, - ids=ids, + ids=None if ids is None else ids if callable(ids) else tuple(ids), name=name, + _ispytest=True, ) # Direct decoration. @@ -1381,8 +1351,60 @@ def pytest_addoption(parser: Parser) -> None: "usefixtures", type="args", default=[], - help="list of default fixtures to be used with this project", + help="List of default fixtures to be used with this project", + ) + group = parser.getgroup("general") + group.addoption( + "--fixtures", + "--funcargs", + action="store_true", + dest="showfixtures", + default=False, + help="Show available fixtures, sorted by plugin appearance " + "(fixtures with leading '_' are only shown with '-v')", ) + group.addoption( + "--fixtures-per-test", + action="store_true", + dest="show_fixtures_per_test", + default=False, + help="Show fixtures per test", + ) + + +def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: + if config.option.showfixtures: + showfixtures(config) + return 0 + if config.option.show_fixtures_per_test: + show_fixtures_per_test(config) + return 0 + return None + + +def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]: + """Return all direct parametrization arguments of a node, so we don't + mistake them for fixtures. + + Check https://github.com/pytest-dev/pytest/issues/5036. + + These things are done later as well when dealing with parametrization + so this could be improved. + """ + parametrize_argnames: Set[str] = set() + for marker in node.iter_markers(name="parametrize"): + if not marker.kwargs.get("indirect", False): + p_argnames, _ = ParameterSet._parse_parametrize_args( + *marker.args, **marker.kwargs + ) + parametrize_argnames.update(p_argnames) + return parametrize_argnames + + +def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]: + """De-duplicate the sequence of names while keeping the original order.""" + # Ideally we would use a set, but it does not preserve insertion order. + return tuple(dict.fromkeys(name for seq in seqs for name in seq)) class FixtureManager: @@ -1416,92 +1438,99 @@ class FixtureManager: by a lookup of their FuncFixtureInfo. """ - FixtureLookupError = FixtureLookupError - FixtureLookupErrorRepr = FixtureLookupErrorRepr - def __init__(self, session: "Session") -> None: self.session = session self.config: Config = session.config - self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} - self._holderobjseen: Set[object] = set() + # Maps a fixture name (argname) to all of the FixtureDefs in the test + # suite/plugins defined with this name. Populated by parsefactories(). + # TODO: The order of the FixtureDefs list of each arg is significant, + # explain. + self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {} + self._holderobjseen: Final[Set[object]] = set() # A mapping from a nodeid to a list of autouse fixtures it defines. - self._nodeid_autousenames: Dict[str, List[str]] = { + self._nodeid_autousenames: Final[Dict[str, List[str]]] = { "": self.config.getini("usefixtures"), } session.config.pluginmanager.register(self, "funcmanage") - def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: - """Return all direct parametrization arguments of a node, so we don't - mistake them for fixtures. + def getfixtureinfo( + self, + node: nodes.Item, + func: Optional[Callable[..., object]], + cls: Optional[type], + ) -> FuncFixtureInfo: + """Calculate the :class:`FuncFixtureInfo` for an item. - Check https://github.com/pytest-dev/pytest/issues/5036. + If ``func`` is None, or if the item sets an attribute + ``nofuncargs = True``, then ``func`` is not examined at all. - These things are done later as well when dealing with parametrization - so this could be improved. + :param node: + The item requesting the fixtures. + :param func: + The item's function. + :param cls: + If the function is a method, the method's class. """ - parametrize_argnames: List[str] = [] - for marker in node.iter_markers(name="parametrize"): - if not marker.kwargs.get("indirect", False): - p_argnames, _ = ParameterSet._parse_parametrize_args( - *marker.args, **marker.kwargs - ) - parametrize_argnames.extend(p_argnames) - - return parametrize_argnames - - def getfixtureinfo( - self, node: nodes.Node, func, cls, funcargs: bool = True - ) -> FuncFixtureInfo: - if funcargs and not getattr(node, "nofuncargs", False): + if func is not None and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) else: argnames = () + usefixturesnames = self._getusefixturesnames(node) + autousenames = self._getautousenames(node) + initialnames = deduplicate_names(autousenames, usefixturesnames, argnames) - usefixtures = tuple( - arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args - ) - initialnames = usefixtures + argnames - fm = node.session._fixturemanager - initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( - initialnames, node, ignore_args=self._get_direct_parametrize_args(node) + direct_parametrize_args = _get_direct_parametrize_args(node) + + names_closure, arg2fixturedefs = self.getfixtureclosure( + parentnode=node, + initialnames=initialnames, + ignore_args=direct_parametrize_args, ) + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) - def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: - nodeid = None - try: - p = absolutepath(plugin.__file__) # type: ignore[attr-defined] - except AttributeError: - pass + def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None: + # Fixtures defined in conftest plugins are only visible to within the + # conftest's directory. This is unlike fixtures in non-conftest plugins + # which have global visibility. So for conftests, construct the base + # nodeid from the plugin name (which is the conftest path). + if plugin_name and plugin_name.endswith("conftest.py"): + # Note: we explicitly do *not* use `plugin.__file__` here -- The + # difference is that plugin_name has the correct capitalization on + # case-insensitive systems (Windows) and other normalization issues + # (issue #11816). + conftestpath = absolutepath(plugin_name) + try: + nodeid = str(conftestpath.parent.relative_to(self.config.rootpath)) + except ValueError: + nodeid = "" + if nodeid == ".": + nodeid = "" + if os.sep != nodes.SEP: + nodeid = nodeid.replace(os.sep, nodes.SEP) else: - # Construct the base nodeid which is later used to check - # what fixtures are visible for particular tests (as denoted - # by their test id). - if p.name.startswith("conftest.py"): - try: - nodeid = str(p.parent.relative_to(self.config.rootpath)) - except ValueError: - nodeid = "" - if nodeid == ".": - nodeid = "" - if os.sep != nodes.SEP: - nodeid = nodeid.replace(os.sep, nodes.SEP) + nodeid = None self.parsefactories(plugin, nodeid) - def _getautousenames(self, nodeid: str) -> Iterator[str]: - """Return the names of autouse fixtures applicable to nodeid.""" - for parentnodeid in nodes.iterparentnodeids(nodeid): - basenames = self._nodeid_autousenames.get(parentnodeid) + def _getautousenames(self, node: nodes.Node) -> Iterator[str]: + """Return the names of autouse fixtures applicable to node.""" + for parentnode in node.listchain(): + basenames = self._nodeid_autousenames.get(parentnode.nodeid) if basenames: yield from basenames + def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: + """Return the names of usefixtures fixtures applicable to node.""" + for mark in node.iter_markers(name="usefixtures"): + yield from mark.args + def getfixtureclosure( self, - fixturenames: Tuple[str, ...], parentnode: nodes.Node, - ignore_args: Sequence[str] = (), - ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: + initialnames: Tuple[str, ...], + ignore_args: AbstractSet[str], + ) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1509,20 +1538,7 @@ class FixtureManager: # to re-discover fixturedefs again for each fixturename # (discovering matching fixtures for a given name/node is expensive). - parentid = parentnode.nodeid - fixturenames_closure = list(self._getautousenames(parentid)) - - def merge(otherlist: Iterable[str]) -> None: - for arg in otherlist: - if arg not in fixturenames_closure: - fixturenames_closure.append(arg) - - merge(fixturenames) - - # At this point, fixturenames_closure contains what we call "initialnames", - # which is a set of fixturenames the function immediately requests. We - # need to return it as well, so save this. - initialnames = tuple(fixturenames_closure) + fixturenames_closure = list(initialnames) arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} lastlen = -1 @@ -1533,10 +1549,12 @@ class FixtureManager: continue if argname in arg2fixturedefs: continue - fixturedefs = self.getfixturedefs(argname, parentid) + fixturedefs = self.getfixturedefs(argname, parentnode) if fixturedefs: arg2fixturedefs[argname] = fixturedefs - merge(fixturedefs[-1].argnames) + for arg in fixturedefs[-1].argnames: + if arg not in fixturenames_closure: + fixturenames_closure.append(arg) def sort_by_scope(arg_name: str) -> Scope: try: @@ -1547,7 +1565,7 @@ class FixtureManager: return fixturedefs[-1]._scope fixturenames_closure.sort(key=sort_by_scope, reverse=True) - return initialnames, fixturenames_closure, arg2fixturedefs + return fixturenames_closure, arg2fixturedefs def pytest_generate_tests(self, metafunc: "Metafunc") -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" @@ -1598,25 +1616,111 @@ class FixtureManager: # Separate parametrized setups. items[:] = reorder_items(items) + def _register_fixture( + self, + *, + name: str, + func: "_FixtureFunc[object]", + nodeid: Optional[str], + scope: Union[ + Scope, _ScopeName, Callable[[str, Config], _ScopeName], None + ] = "function", + params: Optional[Sequence[object]] = None, + ids: Optional[ + Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] + ] = None, + autouse: bool = False, + ) -> None: + """Register a fixture + + :param name: + The fixture's name. + :param func: + The fixture's implementation function. + :param nodeid: + The visibility of the fixture. The fixture will be available to the + node with this nodeid and its children in the collection tree. + None means that the fixture is visible to the entire collection tree, + e.g. a fixture defined for general use in a plugin. + :param scope: + The fixture's scope. + :param params: + The fixture's parametrization params. + :param ids: + The fixture's IDs. + :param autouse: + Whether this is an autouse fixture. + """ + fixture_def = FixtureDef( + config=self.config, + baseid=nodeid, + argname=name, + func=func, + scope=scope, + params=params, + ids=ids, + _ispytest=True, + ) + + faclist = self._arg2fixturedefs.setdefault(name, []) + if fixture_def.has_location: + faclist.append(fixture_def) + else: + # fixturedefs with no location are at the front + # so this inserts the current fixturedef after the + # existing fixturedefs from external plugins but + # before the fixturedefs provided in conftests. + i = len([f for f in faclist if not f.has_location]) + faclist.insert(i, fixture_def) + if autouse: + self._nodeid_autousenames.setdefault(nodeid or "", []).append(name) + + @overload def parsefactories( - self, node_or_obj, nodeid=NOTSET, unittest: bool = False + self, + node_or_obj: nodes.Node, + ) -> None: + raise NotImplementedError() + + @overload + def parsefactories( + self, + node_or_obj: object, + nodeid: Optional[str], ) -> None: + raise NotImplementedError() + + def parsefactories( + self, + node_or_obj: Union[nodes.Node, object], + nodeid: Union[str, NotSetType, None] = NOTSET, + ) -> None: + """Collect fixtures from a collection node or object. + + Found fixtures are parsed into `FixtureDef`s and saved. + + If `node_or_object` is a collection node (with an underlying Python + object), the node's object is traversed and the node's nodeid is used to + determine the fixtures' visibility. `nodeid` must not be specified in + this case. + + If `node_or_object` is an object (e.g. a plugin), the object is + traversed and the given `nodeid` is used to determine the fixtures' + visibility. `nodeid` must be specified in this case; None and "" mean + total visibility. + """ if nodeid is not NOTSET: holderobj = node_or_obj else: - holderobj = node_or_obj.obj + assert isinstance(node_or_obj, nodes.Node) + holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] + assert isinstance(node_or_obj.nodeid, str) nodeid = node_or_obj.nodeid if holderobj in self._holderobjseen: return self._holderobjseen.add(holderobj) - autousenames = [] for name in dir(holderobj): - # ugly workaround for one of the fspath deprecated property of node - # todo: safely generalize - if isinstance(holderobj, nodes.Node) and name == "fspath": - continue - # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getatt() ignores such exceptions. obj = safe_getattr(holderobj, name, None) @@ -1633,54 +1737,176 @@ class FixtureManager: # to issue a warning if called directly, so here we unwrap it in # order to not emit the warning when pytest itself calls the # fixture function. - obj = get_real_method(obj, holderobj) + func = get_real_method(obj, holderobj) - fixture_def = FixtureDef( - fixturemanager=self, - baseid=nodeid, - argname=name, - func=obj, + self._register_fixture( + name=name, + nodeid=nodeid, + func=func, scope=marker.scope, params=marker.params, - unittest=unittest, ids=marker.ids, + autouse=marker.autouse, ) - faclist = self._arg2fixturedefs.setdefault(name, []) - if fixture_def.has_location: - faclist.append(fixture_def) - else: - # fixturedefs with no location are at the front - # so this inserts the current fixturedef after the - # existing fixturedefs from external plugins but - # before the fixturedefs provided in conftests. - i = len([f for f in faclist if not f.has_location]) - faclist.insert(i, fixture_def) - if marker.autouse: - autousenames.append(name) - - if autousenames: - self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames) - def getfixturedefs( - self, argname: str, nodeid: str + self, argname: str, node: nodes.Node ) -> Optional[Sequence[FixtureDef[Any]]]: - """Get a list of fixtures which are applicable to the given node id. + """Get FixtureDefs for a fixture name which are applicable + to a given node. + + Returns None if there are no fixtures at all defined with the given + name. (This is different from the case in which there are fixtures + with the given name, but none applicable to the node. In this case, + an empty result is returned). - :param str argname: Name of the fixture to search for. - :param str nodeid: Full node id of the requesting test. - :rtype: Sequence[FixtureDef] + :param argname: Name of the fixture to search for. + :param node: The requesting Node. """ try: fixturedefs = self._arg2fixturedefs[argname] except KeyError: return None - return tuple(self._matchfactories(fixturedefs, nodeid)) + return tuple(self._matchfactories(fixturedefs, node)) def _matchfactories( - self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str + self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node ) -> Iterator[FixtureDef[Any]]: - parentnodeids = set(nodes.iterparentnodeids(nodeid)) + parentnodeids = {n.nodeid for n in node.iter_parents()} for fixturedef in fixturedefs: if fixturedef.baseid in parentnodeids: yield fixturedef + + +def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]: + from _pytest.main import wrap_session + + return wrap_session(config, _show_fixtures_per_test) + + +_PYTEST_DIR = Path(_pytest.__file__).parent + + +def _pretty_fixture_path(invocation_dir: Path, func) -> str: + loc = Path(getlocation(func, invocation_dir)) + prefix = Path("...", "_pytest") + try: + return str(prefix / loc.relative_to(_PYTEST_DIR)) + except ValueError: + return bestrelpath(invocation_dir, loc) + + +def _show_fixtures_per_test(config: Config, session: "Session") -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.getvalue("verbose") + + def get_best_relpath(func) -> str: + loc = getlocation(func, invocation_dir) + return bestrelpath(invocation_dir, Path(loc)) + + def write_fixture(fixture_def: FixtureDef[object]) -> None: + argname = fixture_def.argname + if verbose <= 0 and argname.startswith("_"): + return + prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) + tw.write(f"{argname}", green=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + fixture_doc = inspect.getdoc(fixture_def.func) + if fixture_doc: + write_docstring( + tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc + ) + else: + tw.line(" no docstring available", red=True) + + def write_item(item: nodes.Item) -> None: + # Not all items have _fixtureinfo attribute. + info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) + if info is None or not info.name2fixturedefs: + # This test item does not use any fixtures. + return + tw.line() + tw.sep("-", f"fixtures used by {item.name}") + # TODO: Fix this type ignore. + tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined] + # dict key not used in loop but needed for sorting. + for _, fixturedefs in sorted(info.name2fixturedefs.items()): + assert fixturedefs is not None + if not fixturedefs: + continue + # Last item is expected to be the one used by the test item. + write_fixture(fixturedefs[-1]) + + for session_item in session.items: + write_item(session_item) + + +def showfixtures(config: Config) -> Union[int, ExitCode]: + from _pytest.main import wrap_session + + return wrap_session(config, _showfixtures_main) + + +def _showfixtures_main(config: Config, session: "Session") -> None: + import _pytest.config + + session.perform_collect() + invocation_dir = config.invocation_params.dir + tw = _pytest.config.create_terminal_writer(config) + verbose = config.getvalue("verbose") + + fm = session._fixturemanager + + available = [] + seen: Set[Tuple[str, str]] = set() + + for argname, fixturedefs in fm._arg2fixturedefs.items(): + assert fixturedefs is not None + if not fixturedefs: + continue + for fixturedef in fixturedefs: + loc = getlocation(fixturedef.func, invocation_dir) + if (fixturedef.argname, loc) in seen: + continue + seen.add((fixturedef.argname, loc)) + available.append( + ( + len(fixturedef.baseid), + fixturedef.func.__module__, + _pretty_fixture_path(invocation_dir, fixturedef.func), + fixturedef.argname, + fixturedef, + ) + ) + + available.sort() + currentmodule = None + for baseid, module, prettypath, argname, fixturedef in available: + if currentmodule != module: + if not module.startswith("_pytest."): + tw.line() + tw.sep("-", f"fixtures defined from {module}") + currentmodule = module + if verbose <= 0 and argname.startswith("_"): + continue + tw.write(f"{argname}", green=True) + if fixturedef.scope != "function": + tw.write(" [%s scope]" % fixturedef.scope, cyan=True) + tw.write(f" -- {prettypath}", yellow=True) + tw.write("\n") + doc = inspect.getdoc(fixturedef.func) + if doc: + write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) + else: + tw.line(" no docstring available", red=True) + tw.line() + + +def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: + for line in doc.split("\n"): + tw.line(indent + line) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/freeze_support.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/freeze_support.py index 9f8ea231fed..e03a6d1753d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/freeze_support.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/freeze_support.py @@ -1,5 +1,6 @@ """Provides a function to report all internal modules for using freezing tools.""" + import types from typing import Iterator from typing import List @@ -34,7 +35,7 @@ def _iter_all_modules( else: # Type ignored because typeshed doesn't define ModuleType.__path__ # (only defined on packages). - package_path = package.__path__ # type: ignore[attr-defined] + package_path = package.__path__ path, prefix = package_path[0], package.__name__ + "." for _, name, is_package in pkgutil.iter_modules([path]): if is_package: diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/helpconfig.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/helpconfig.py index aca2cd391e4..37fbdf04d7e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/helpconfig.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/helpconfig.py @@ -1,16 +1,20 @@ +# mypy: allow-untyped-defs """Version info, help messages, tracing configuration.""" + +from argparse import Action import os import sys -from argparse import Action +from typing import Generator from typing import List from typing import Optional from typing import Union -import pytest from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PrintHelp from _pytest.config.argparsing import Parser +from _pytest.terminal import TerminalReporter +import pytest class HelpAction(Action): @@ -49,7 +53,7 @@ def pytest_addoption(parser: Parser) -> None: action="count", default=0, dest="version", - help="display pytest version and information about plugins. " + help="Display pytest version and information about plugins. " "When given twice, also display information about plugins.", ) group._addoption( @@ -57,7 +61,7 @@ def pytest_addoption(parser: Parser) -> None: "--help", action=HelpAction, dest="help", - help="show help message and configuration info", + help="Show help message and configuration info", ) group._addoption( "-p", @@ -65,7 +69,7 @@ def pytest_addoption(parser: Parser) -> None: dest="plugins", default=[], metavar="name", - help="early-load given plugin module name or entry point (multi-allowed).\n" + help="Early-load given plugin module name or entry point (multi-allowed). " "To avoid loading of plugins, use the `no:` prefix, e.g. " "`no:doctest`.", ) @@ -74,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None: "--trace-config", action="store_true", default=False, - help="trace considerations of conftest.py files.", + help="Trace considerations of conftest.py files", ) group.addoption( "--debug", @@ -83,34 +87,34 @@ def pytest_addoption(parser: Parser) -> None: const="pytestdebug.log", dest="debug", metavar="DEBUG_FILE_NAME", - help="store internal tracing debug information in this log file.\n" - "This file is opened with 'w' and truncated as a result, care advised.\n" - "Defaults to 'pytestdebug.log'.", + help="Store internal tracing debug information in this log file. " + "This file is opened with 'w' and truncated as a result, care advised. " + "Default: pytestdebug.log.", ) group._addoption( "-o", "--override-ini", dest="override_ini", action="append", - help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.', + help='Override ini option with "option=value" style, ' + "e.g. `-o xfail_strict=True -o cache_dir=cache`.", ) -@pytest.hookimpl(hookwrapper=True) -def pytest_cmdline_parse(): - outcome = yield - config: Config = outcome.get_result() +@pytest.hookimpl(wrapper=True) +def pytest_cmdline_parse() -> Generator[None, Config, Config]: + config = yield if config.option.debug: # --debug | --debug <file.log> was provided. path = config.option.debug - debugfile = open(path, "w") + debugfile = open(path, "w", encoding="utf-8") debugfile.write( - "versions pytest-%s, " - "python-%s\ncwd=%s\nargs=%s\n\n" - % ( + "versions pytest-{}, " + "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format( pytest.__version__, ".".join(map(str, sys.version_info)), + config.invocation_params.dir, os.getcwd(), config.invocation_params.args, ) @@ -127,13 +131,13 @@ def pytest_cmdline_parse(): config.add_cleanup(unset_tracing) + return config + def showversion(config: Config) -> None: if config.option.version > 1: sys.stdout.write( - "This is pytest version {}, imported from {}\n".format( - pytest.__version__, pytest.__file__ - ) + f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n" ) plugininfo = getpluginversioninfo(config) if plugininfo: @@ -158,12 +162,16 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: def showhelp(config: Config) -> None: import textwrap - reporter = config.pluginmanager.get_plugin("terminalreporter") + reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin( + "terminalreporter" + ) + assert reporter is not None tw = reporter._tw tw.write(config._parser.optparser.format_help()) tw.line() tw.line( - "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:" + "[pytest] ini-options in the first " + "pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:" ) tw.line() @@ -203,12 +211,12 @@ def showhelp(config: Config) -> None: tw.line(indent + line) tw.line() - tw.line("environment variables:") + tw.line("Environment variables:") vars = [ - ("PYTEST_ADDOPTS", "extra command line options"), - ("PYTEST_PLUGINS", "comma-separated plugins to load during startup"), - ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"), - ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"), + ("PYTEST_ADDOPTS", "Extra command line options"), + ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"), + ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"), + ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"), ] for name, help in vars: tw.line(f" {name:<24} {help}") diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/hookspec.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/hookspec.py index 79251315d89..9ec9b3b5e10 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/hookspec.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/hookspec.py @@ -1,5 +1,7 @@ +# mypy: allow-untyped-defs """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" + from pathlib import Path from typing import Any from typing import Dict @@ -13,20 +15,21 @@ from typing import Union from pluggy import HookspecMarker -from _pytest.deprecated import WARNING_CAPTURED_HOOK -from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK +from .deprecated import HOOK_LEGACY_PATH_ARG + if TYPE_CHECKING: import pdb + from typing import Literal import warnings - from typing_extensions import Literal + from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr - from _pytest.code import ExceptionInfo + from _pytest.compat import LEGACY_PATH + from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager - from _pytest.config import _PluggyPlugin from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef from _pytest.fixtures import SubRequest @@ -34,15 +37,15 @@ if TYPE_CHECKING: from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import Exit + from _pytest.python import Class from _pytest.python import Function from _pytest.python import Metafunc from _pytest.python import Module - from _pytest.python import PyCollector from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.terminal import TerminalReporter - from _pytest.compat import LEGACY_PATH + from _pytest.terminal import TestShortLogReport hookspec = HookspecMarker("pytest") @@ -55,26 +58,43 @@ hookspec = HookspecMarker("pytest") @hookspec(historic=True) def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: """Called at plugin registration time to allow adding new hooks via a call to - ``pluginmanager.add_hookspecs(module_or_class, prefix)``. + :func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`. - :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. + :param pluginmanager: The pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. """ @hookspec(historic=True) def pytest_plugin_registered( - plugin: "_PluggyPlugin", manager: "PytestPluginManager" + plugin: "_PluggyPlugin", + plugin_name: str, + manager: "PytestPluginManager", ) -> None: """A new pytest plugin got registered. :param plugin: The plugin module or instance. - :param pytest.PytestPluginManager manager: pytest plugin manager. + :param plugin_name: The name by which the plugin is registered. + :param manager: The pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered, once for each plugin registered thus far + (including itself!), and for all plugins thereafter when they are + registered. """ @@ -83,21 +103,15 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> """Register argparse-style options and ini-style config values, called once at the beginning of a test run. - .. note:: - - This function should be implemented only in plugins or ``conftest.py`` - files situated at the tests root directory due to how pytest - :ref:`discovers plugins during startup <pluginorder>`. - - :param pytest.Parser parser: + :param parser: To add command line options, call :py:func:`parser.addoption(...) <pytest.Parser.addoption>`. To add ini-file values call :py:func:`parser.addini(...) <pytest.Parser.addini>`. - :param pytest.PytestPluginManager pluginmanager: - The pytest plugin manager, which can be used to install :py:func:`hookspec`'s - or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks + :param pluginmanager: + The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s + or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks to change how command line options are added. Options can later be accessed through the @@ -113,7 +127,15 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> attribute or can be retrieved as the ``pytestconfig`` fixture. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. + + Use in conftest plugins + ======================= + + If a conftest plugin implements this hook, it will be called immediately + when the conftest is registered. + + This hook is only called for :ref:`initial conftests <pluginorder>`. """ @@ -121,16 +143,17 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> def pytest_configure(config: "Config") -> None: """Allow plugins and conftest files to perform initial configuration. - This hook is called for every plugin and initial conftest file - after command line options have been parsed. + .. note:: + This hook is incompatible with hook wrappers. - After that, the hook is called for other conftest files as they are - imported. + :param config: The pytest config object. - .. note:: - This hook is incompatible with ``hookwrapper=True``. + Use in conftest plugins + ======================= - :param pytest.Config config: The pytest config object. + This hook is called for every :ref:`initial conftest <pluginorder>` file + after command line options have been parsed. After that, the hook is called + for other conftest files as they are registered. """ @@ -144,58 +167,59 @@ def pytest_configure(config: "Config") -> None: def pytest_cmdline_parse( pluginmanager: "PytestPluginManager", args: List[str] ) -> Optional["Config"]: - """Return an initialized config object, parsing the specified args. + """Return an initialized :class:`~pytest.Config`, parsing the specified args. Stops at first non-None result, see :ref:`firstresult`. .. note:: - This hook will only be called for plugin classes passed to the + This hook is only called for plugin classes passed to the ``plugins`` arg when using `pytest.main`_ to perform an in-process test run. - :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. - :param List[str] args: List of arguments passed on the command line. + :param pluginmanager: The pytest plugin manager. + :param args: List of arguments passed on the command line. + :returns: A pytest config object. + + Use in conftest plugins + ======================= + + This hook is not called for conftest files. """ -@hookspec(warn_on_impl=WARNING_CMDLINE_PREPARSE_HOOK) -def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: - """(**Deprecated**) modify command line arguments before option parsing. +def pytest_load_initial_conftests( + early_config: "Config", parser: "Parser", args: List[str] +) -> None: + """Called to implement the loading of :ref:`initial conftest files + <pluginorder>` ahead of command line option parsing. - This hook is considered deprecated and will be removed in a future pytest version. Consider - using :hook:`pytest_load_initial_conftests` instead. + :param early_config: The pytest config object. + :param args: Arguments passed on the command line. + :param parser: To add command line options. - .. note:: - This hook will not be called for ``conftest.py`` files, only for setuptools plugins. + Use in conftest plugins + ======================= - :param pytest.Config config: The pytest config object. - :param List[str] args: Arguments passed on the command line. + This hook is not called for conftest files. """ @hookspec(firstresult=True) def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: - """Called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. + """Called for performing the main command line action. - Stops at first non-None result, see :ref:`firstresult`. - - :param pytest.Config config: The pytest config object. - """ + The default implementation will invoke the configure hooks and + :hook:`pytest_runtestloop`. + Stops at first non-None result, see :ref:`firstresult`. -def pytest_load_initial_conftests( - early_config: "Config", parser: "Parser", args: List[str] -) -> None: - """Called to implement the loading of initial conftest files ahead - of command line option parsing. + :param config: The pytest config object. + :returns: The exit code. - .. note:: - This hook will not be called for ``conftest.py`` files, only for setuptools plugins. + Use in conftest plugins + ======================= - :param pytest.Config early_config: The pytest config object. - :param List[str] args: Arguments passed on the command line. - :param pytest.Parser parser: To add command line options. + This hook is only called for :ref:`initial conftests <pluginorder>`. """ @@ -237,7 +261,12 @@ def pytest_collection(session: "Session") -> Optional[object]: for example the terminal plugin uses it to start displaying the collection counter (and returns `None`). - :param pytest.Session session: The pytest session object. + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests <pluginorder>`. """ @@ -247,20 +276,37 @@ def pytest_collection_modifyitems( """Called after collection has been performed. May filter or re-order the items in-place. - :param pytest.Session session: The pytest session object. - :param pytest.Config config: The pytest config object. - :param List[pytest.Item] items: List of item objects. + :param session: The pytest session object. + :param config: The pytest config object. + :param items: List of item objects. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ def pytest_collection_finish(session: "Session") -> None: """Called after collection has been performed and modified. - :param pytest.Session session: The pytest session object. + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ -@hookspec(firstresult=True) +@hookspec( + firstresult=True, + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="collection_path" + ), + }, +) def pytest_ignore_collect( collection_path: Path, path: "LEGACY_PATH", config: "Config" ) -> Optional[bool]: @@ -271,31 +317,84 @@ def pytest_ignore_collect( Stops at first non-None result, see :ref:`firstresult`. - :param pathlib.Path collection_path : The path to analyze. - :param LEGACY_PATH path: The path to analyze (deprecated). - :param pytest.Config config: The pytest config object. + :param collection_path: The path to analyze. + :param path: The path to analyze (deprecated). + :param config: The pytest config object. .. versionchanged:: 7.0.0 The ``collection_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. The ``path`` parameter has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot ignore itself!). + """ + + +@hookspec(firstresult=True) +def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Collector]": + """Create a :class:`~pytest.Collector` for the given directory, or None if + not relevant. + + .. versionadded:: 8.0 + + For best results, the returned collector should be a subclass of + :class:`~pytest.Directory`, but this is not required. + + The new node needs to have the specified ``parent`` as a parent. + + Stops at first non-None result, see :ref:`firstresult`. + + :param path: The path to analyze. + + See :ref:`custom directory collectors` for a simple example of use of this + hook. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collection path, only + conftest files in parent directories of the collection path are consulted + (if the path is a directory, its own conftest file is *not* consulted - a + directory cannot collect itself!). """ +@hookspec( + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="file_path" + ), + }, +) def pytest_collect_file( file_path: Path, path: "LEGACY_PATH", parent: "Collector" ) -> "Optional[Collector]": - """Create a Collector for the given path, or None if not relevant. + """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. + + For best results, the returned collector should be a subclass of + :class:`~pytest.File`, but this is not required. The new node needs to have the specified ``parent`` as a parent. - :param pathlib.Path file_path: The path to analyze. - :param LEGACY_PATH path: The path to collect (deprecated). + :param file_path: The path to analyze. + :param path: The path to collect (deprecated). .. versionchanged:: 7.0.0 The ``file_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. The ``path`` parameter has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given file path, only + conftest files in parent directories of the file path are consulted. """ @@ -303,21 +402,61 @@ def pytest_collect_file( def pytest_collectstart(collector: "Collector") -> None: - """Collector starts collecting.""" + """Collector starts collecting. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ def pytest_itemcollected(item: "Item") -> None: - """We just collected a test item.""" + """We just collected a test item. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. + """ def pytest_collectreport(report: "CollectReport") -> None: - """Collector finished collecting.""" + """Collector finished collecting. + + :param report: + The collect report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. + """ def pytest_deselected(items: Sequence["Item"]) -> None: """Called for deselected test items, e.g. by keyword. May be called multiple times. + + :param items: + The items. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ @@ -327,6 +466,16 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor a :class:`~pytest.CollectReport`. Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The collector. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories are + consulted. """ @@ -335,36 +484,66 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor # ------------------------------------------------------------------------- -@hookspec(firstresult=True) +@hookspec( + firstresult=True, + warn_on_impl_args={ + "path": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="path", pathlib_path_arg="module_path" + ), + }, +) def pytest_pycollect_makemodule( module_path: Path, path: "LEGACY_PATH", parent ) -> Optional["Module"]: - """Return a Module collector or None for the given path. + """Return a :class:`pytest.Module` collector or None for the given path. This hook will be called for each matching test module path. - The pytest_collect_file hook needs to be used if you want to + The :hook:`pytest_collect_file` hook needs to be used if you want to create test modules for files that do not match as a test module. Stops at first non-None result, see :ref:`firstresult`. - :param pathlib.Path module_path: The path of the module to collect. - :param LEGACY_PATH path: The path of the module to collect (deprecated). + :param module_path: The path of the module to collect. + :param path: The path of the module to collect (deprecated). .. versionchanged:: 7.0.0 The ``module_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. The ``path`` parameter has been deprecated in favor of ``fspath``. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given parent collector, + only conftest files in the collector's directory and its parent directories + are consulted. """ @hookspec(firstresult=True) def pytest_pycollect_makeitem( - collector: "PyCollector", name: str, obj: object + collector: Union["Module", "Class"], name: str, obj: object ) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]: """Return a custom item/collector for a Python object in a module, or None. Stops at first non-None result, see :ref:`firstresult`. + + :param collector: + The module/class collector. + :param name: + The name of the object in the module/class. + :param obj: + The object. + :returns: + The created items/collectors. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given collector, only + conftest files in the collector's directory and its parent directories + are consulted. """ @@ -373,11 +552,32 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: """Call underlying test function. Stops at first non-None result, see :ref:`firstresult`. + + :param pyfuncitem: + The function item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only + conftest files in the item's directory and its parent directories + are consulted. """ def pytest_generate_tests(metafunc: "Metafunc") -> None: - """Generate (multiple) parametrized calls to a test function.""" + """Generate (multiple) parametrized calls to a test function. + + :param metafunc: + The :class:`~pytest.Metafunc` helper for the test function. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given function definition, + only conftest files in the functions's directory and its parent directories + are consulted. + """ @hookspec(firstresult=True) @@ -392,9 +592,14 @@ def pytest_make_parametrize_id( Stops at first non-None result, see :ref:`firstresult`. - :param pytest.Config config: The pytest config object. + :param config: The pytest config object. :param val: The parametrized value. - :param str argname: The automatic parameter name produced by pytest. + :param argname: The automatic parameter name produced by pytest. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ @@ -417,10 +622,15 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the loop is terminated after the runtest protocol for the current item is finished. - :param pytest.Session session: The pytest session object. + :param session: The pytest session object. Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ @@ -440,7 +650,7 @@ def pytest_runtest_protocol( - ``pytest_runtest_logreport(report)`` - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set: + - Call phase, if the setup passed and the ``setuponly`` pytest option is not set: - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) - ``report = pytest_runtest_makereport(item, call)`` - ``pytest_runtest_logreport(report)`` @@ -459,6 +669,11 @@ def pytest_runtest_protocol( Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ @@ -469,8 +684,16 @@ def pytest_runtest_logstart( See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. - :param str nodeid: Full node ID of the item. - :param location: A tuple of ``(filename, lineno, testname)``. + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -481,8 +704,16 @@ def pytest_runtest_logfinish( See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. - :param str nodeid: Full node ID of the item. - :param location: A tuple of ``(filename, lineno, testname)``. + :param nodeid: Full node ID of the item. + :param location: A tuple of ``(filename, lineno, testname)`` + where ``filename`` is a file path relative to ``config.rootpath`` + and ``lineno`` is 0-based. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -493,6 +724,15 @@ def pytest_runtest_setup(item: "Item") -> None: parents (which haven't been setup yet). This includes obtaining the values of fixtures required by the item (which haven't been obtained yet). + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -500,6 +740,15 @@ def pytest_runtest_call(item: "Item") -> None: """Called to run the test for test item (the call phase). The default implementation calls ``item.runtest()``. + + :param item: + The item. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -511,11 +760,19 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: includes running the teardown phase of fixtures required by the item (if they go out of scope). + :param item: + The item. :param nextitem: The scheduled-to-be-next test item (None if no further test item is scheduled). This argument is used to perform exact teardowns, i.e. calling just enough finalizers so that nextitem only needs to call setup functions. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -528,9 +785,16 @@ def pytest_runtest_makereport( See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + :param item: The item. :param call: The :class:`~pytest.CallInfo` for the phase. Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -539,6 +803,12 @@ def pytest_runtest_logreport(report: "TestReport") -> None: of the setup, call and teardown runtest phases of an item. See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -548,7 +818,17 @@ def pytest_report_to_serializable( report: Union["CollectReport", "TestReport"], ) -> Optional[Dict[str, Any]]: """Serialize the given report object into a data structure suitable for - sending over the wire, e.g. converted to JSON.""" + sending over the wire, e.g. converted to JSON. + + :param config: The pytest config object. + :param report: The report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ @hookspec(firstresult=True) @@ -557,7 +837,16 @@ def pytest_report_from_serializable( data: Dict[str, Any], ) -> Optional[Union["CollectReport", "TestReport"]]: """Restore a report object previously serialized with - :hook:`pytest_report_to_serializable`.""" + :hook:`pytest_report_to_serializable`. + + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. The exact details may depend + on the plugin which calls the hook. + """ # ------------------------------------------------------------------------- @@ -571,7 +860,12 @@ def pytest_fixture_setup( ) -> Optional[object]: """Perform fixture setup execution. - :returns: The return value of the call to the fixture function. + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + :returns: + The return value of the call to the fixture function. Stops at first non-None result, see :ref:`firstresult`. @@ -579,6 +873,13 @@ def pytest_fixture_setup( If the fixture function returns None, other implementations of this hook function will continue to be called, according to the behavior of the :ref:`firstresult` option. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. """ @@ -587,7 +888,20 @@ def pytest_fixture_post_finalizer( ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not - ``None``).""" + ``None``). + + :param fixturedef: + The fixture definition object. + :param request: + The fixture request object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given fixture, only + conftest files in the fixture scope's directory and its parent directories + are consulted. + """ # ------------------------------------------------------------------------- @@ -599,7 +913,12 @@ def pytest_sessionstart(session: "Session") -> None: """Called after the ``Session`` object has been created and before performing collection and entering the run test loop. - :param pytest.Session session: The pytest session object. + :param session: The pytest session object. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests <pluginorder>`. """ @@ -609,15 +928,25 @@ def pytest_sessionfinish( ) -> None: """Called after whole test run finished, right before returning the exit status to the system. - :param pytest.Session session: The pytest session object. - :param int exitstatus: The status which pytest will return to the system. + :param session: The pytest session object. + :param exitstatus: The status which pytest will return to the system. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ def pytest_unconfigure(config: "Config") -> None: """Called before test process is exited. - :param pytest.Config config: The pytest config object. + :param config: The pytest config object. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. """ @@ -636,7 +965,16 @@ def pytest_assertrepr_compare( *in* a string will be escaped. Note that all but the first line will be indented slightly, the intention is for the first line to be a summary. - :param pytest.Config config: The pytest config object. + :param config: The pytest config object. + :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`. + :param left: The left operand. + :param right: The right operand. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -661,10 +999,16 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No You need to **clean the .pyc** files in your project directory and interpreter libraries when enabling this option, as assertions will require to be re-written. - :param pytest.Item item: pytest item object of current test. - :param int lineno: Line number of the assert statement. - :param str orig: String with the original assertion. - :param str expl: String with the assert explanation. + :param item: pytest item object of current test. + :param lineno: Line number of the assert statement. + :param orig: String with the original assertion. + :param expl: String with the assert explanation. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in the item's directory and its parent directories are consulted. """ @@ -673,14 +1017,21 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No # ------------------------------------------------------------------------- -def pytest_report_header( +@hookspec( + warn_on_impl_args={ + "startdir": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="startdir", pathlib_path_arg="start_path" + ), + }, +) +def pytest_report_header( # type:ignore[empty-body] config: "Config", start_path: Path, startdir: "LEGACY_PATH" ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed as header info for terminal reporting. - :param pytest.Config config: The pytest config object. - :param Path start_path: The starting dir. - :param LEGACY_PATH startdir: The starting dir (deprecated). + :param config: The pytest config object. + :param start_path: The starting dir. + :param startdir: The starting dir (deprecated). .. note:: @@ -689,20 +1040,26 @@ def pytest_report_header( If you want to have your line(s) displayed first, use :ref:`trylast=True <plugin-hookorder>`. - .. note:: - - This function should be implemented only in plugins or ``conftest.py`` - files situated at the tests root directory due to how pytest - :ref:`discovers plugins during startup <pluginorder>`. - .. versionchanged:: 7.0.0 The ``start_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``startdir`` parameter. The ``startdir`` parameter has been deprecated. + + Use in conftest plugins + ======================= + + This hook is only called for :ref:`initial conftests <pluginorder>`. """ -def pytest_report_collectionfinish( +@hookspec( + warn_on_impl_args={ + "startdir": HOOK_LEGACY_PATH_ARG.format( + pylib_path_arg="startdir", pathlib_path_arg="start_path" + ), + }, +) +def pytest_report_collectionfinish( # type:ignore[empty-body] config: "Config", start_path: Path, startdir: "LEGACY_PATH", @@ -715,9 +1072,9 @@ def pytest_report_collectionfinish( .. versionadded:: 3.2 - :param pytest.Config config: The pytest config object. - :param Path start_path: The starting dir. - :param LEGACY_PATH startdir: The starting dir (deprecated). + :param config: The pytest config object. + :param start_path: The starting dir. + :param startdir: The starting dir (deprecated). :param items: List of pytest items that are going to be executed; this list should not be modified. .. note:: @@ -731,13 +1088,18 @@ def pytest_report_collectionfinish( The ``start_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``startdir`` parameter. The ``startdir`` parameter has been deprecated. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ @hookspec(firstresult=True) -def pytest_report_teststatus( +def pytest_report_teststatus( # type:ignore[empty-body] report: Union["CollectReport", "TestReport"], config: "Config" -) -> Tuple[str, str, Union[str, Mapping[str, bool]]]: +) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]": """Return result-category, shortletter and verbose word for status reporting. @@ -756,8 +1118,14 @@ def pytest_report_teststatus( :param report: The report object whose status is to be returned. :param config: The pytest config object. + :returns: The test status. Stops at first non-None result, see :ref:`firstresult`. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ @@ -768,47 +1136,17 @@ def pytest_terminal_summary( ) -> None: """Add a section to terminal summary reporting. - :param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object. - :param int exitstatus: The exit status that will be reported back to the OS. - :param pytest.Config config: The pytest config object. + :param terminalreporter: The internal terminal reporter object. + :param exitstatus: The exit status that will be reported back to the OS. + :param config: The pytest config object. .. versionadded:: 4.2 The ``config`` parameter. - """ - - -@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) -def pytest_warning_captured( - warning_message: "warnings.WarningMessage", - when: "Literal['config', 'collect', 'runtest']", - item: Optional["Item"], - location: Optional[Tuple[str, int, str]], -) -> None: - """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. - - .. deprecated:: 6.0 - - This hook is considered deprecated and will be removed in a future pytest version. - Use :func:`pytest_warning_recorded` instead. - - :param warnings.WarningMessage warning_message: - The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains - the same attributes as the parameters of :py:func:`warnings.showwarning`. - - :param str when: - Indicates when the warning was captured. Possible values: - - * ``"config"``: during pytest configuration/initialization stage. - * ``"collect"``: during test collection. - * ``"runtest"``: during test execution. - :param pytest.Item|None item: - The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. + Use in conftest plugins + ======================= - :param tuple location: - When available, holds information about the execution context of the captured - warning (filename, linenumber, function). ``function`` evaluates to <module> - when the execution context is at the module level. + Any conftest plugin can implement this hook. """ @@ -821,26 +1159,34 @@ def pytest_warning_recorded( ) -> None: """Process a warning captured by the internal pytest warnings plugin. - :param warnings.WarningMessage warning_message: - The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains - the same attributes as the parameters of :py:func:`warnings.showwarning`. + :param warning_message: + The captured warning. This is the same object produced by :class:`warnings.catch_warnings`, + and contains the same attributes as the parameters of :py:func:`warnings.showwarning`. - :param str when: + :param when: Indicates when the warning was captured. Possible values: * ``"config"``: during pytest configuration/initialization stage. * ``"collect"``: during test collection. * ``"runtest"``: during test execution. - :param str nodeid: - Full id of the item. + :param nodeid: + Full id of the item. Empty string for warnings that are not specific to + a particular node. - :param tuple|None location: + :param location: When available, holds information about the execution context of the captured warning (filename, linenumber, function). ``function`` evaluates to <module> when the execution context is at the module level. .. versionadded:: 6.0 + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. If the warning is specific to a + particular node, only conftest files in parent directories of the node are + consulted. """ @@ -849,7 +1195,9 @@ def pytest_warning_recorded( # ------------------------------------------------------------------------- -def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]: +def pytest_markeval_namespace( # type:ignore[empty-body] + config: "Config", +) -> Dict[str, Any]: """Called when constructing the globals dictionary used for evaluating string conditions in xfail/skipif markers. @@ -860,8 +1208,14 @@ def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]: .. versionadded:: 6.2 - :param pytest.Config config: The pytest config object. + :param config: The pytest config object. :returns: A dictionary of additional globals to add. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given item, only conftest + files in parent directories of the item are consulted. """ @@ -878,13 +1232,29 @@ def pytest_internalerror( Return True to suppress the fallback handling of printing an INTERNALERROR message directly to sys.stderr. + + :param excrepr: The exception repr object. + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ def pytest_keyboard_interrupt( excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", ) -> None: - """Called for keyboard interrupt.""" + """Called for keyboard interrupt. + + :param excinfo: The exception info. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. + """ def pytest_exception_interact( @@ -896,13 +1266,26 @@ def pytest_exception_interact( interactively handled. May be called during collection (see :hook:`pytest_make_collect_report`), - in which case ``report`` is a :class:`CollectReport`. + in which case ``report`` is a :class:`~pytest.CollectReport`. May be called during runtest of an item (see :hook:`pytest_runtest_protocol`), - in which case ``report`` is a :class:`TestReport`. + in which case ``report`` is a :class:`~pytest.TestReport`. This hook is not called if the exception that was raised is an internal exception like ``skip.Exception``. + + :param node: + The item or collector. + :param call: + The call information. Contains the exception. + :param report: + The collection or test report. + + Use in conftest plugins + ======================= + + Any conftest file can implement this hook. For a given node, only conftest + files in parent directories of the node are consulted. """ @@ -912,8 +1295,13 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: Can be used by plugins to take special action just before the python debugger enters interactive mode. - :param pytest.Config config: The pytest config object. - :param pdb.Pdb pdb: The Pdb instance. + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ @@ -923,6 +1311,11 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None: Can be used by plugins to take special action just after the python debugger leaves interactive mode. - :param pytest.Config config: The pytest config object. - :param pdb.Pdb pdb: The Pdb instance. + :param config: The pytest config object. + :param pdb: The Pdb instance. + + Use in conftest plugins + ======================= + + Any conftest plugin can implement this hook. """ diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/junitxml.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/junitxml.py index 4af5fbab0c0..13fc9277aec 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/junitxml.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/junitxml.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs """Report test results in JUnit-XML format, for use with Jenkins and build integration servers. @@ -6,12 +7,12 @@ Based on initial code from Ross Lawley. Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ + +from datetime import datetime import functools import os import platform import re -import xml.etree.ElementTree as ET -from datetime import datetime from typing import Callable from typing import Dict from typing import List @@ -19,8 +20,8 @@ from typing import Match from typing import Optional from typing import Tuple from typing import Union +import xml.etree.ElementTree as ET -import pytest from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr @@ -32,6 +33,7 @@ from _pytest.fixtures import FixtureRequest from _pytest.reports import TestReport from _pytest.stash import StashKey from _pytest.terminal import TerminalReporter +import pytest xml_key = StashKey["LogXML"]() @@ -59,7 +61,7 @@ def bin_xml_escape(arg: object) -> str: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] # For an unknown(?) reason, we disallow #x7F (DEL) as well. illegal_xml_re = ( - "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]" + "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]" ) return re.sub(illegal_xml_re, repl, str(arg)) @@ -92,7 +94,7 @@ class _NodeReporter: self.xml = xml self.add_stats = self.xml.add_stats self.family = self.xml.family - self.duration = 0 + self.duration = 0.0 self.properties: List[Tuple[str, str]] = [] self.nodes: List[ET.Element] = [] self.attrs: Dict[str, str] = {} @@ -141,7 +143,7 @@ class _NodeReporter: # Filter out attributes not permitted by this test family. # Including custom attributes because they are not valid here. temp_attrs = {} - for key in self.attrs.keys(): + for key in self.attrs: if key in families[self.family]["testcase"]: temp_attrs[key] = self.attrs[key] self.attrs = temp_attrs @@ -231,7 +233,7 @@ class _NodeReporter: msg = f'failed on teardown with "{reason}"' else: msg = f'failed on setup with "{reason}"' - self._add_simple("error", msg, str(report.longrepr)) + self._add_simple("error", bin_xml_escape(msg), str(report.longrepr)) def append_skipped(self, report: TestReport) -> None: if hasattr(report, "wasxfail"): @@ -248,7 +250,9 @@ class _NodeReporter: skipreason = skipreason[9:] details = f"{filename}:{lineno}: {skipreason}" - skipped = ET.Element("skipped", type="pytest.skip", message=skipreason) + skipped = ET.Element( + "skipped", type="pytest.skip", message=bin_xml_escape(skipreason) + ) skipped.text = bin_xml_escape(details) self.append(skipped) self.write_captured_output(report) @@ -258,7 +262,7 @@ class _NodeReporter: self.__dict__.clear() # Type ignored because mypy doesn't like overriding a method. # Also the return value doesn't match... - self.to_xml = lambda: data # type: ignore[assignment] + self.to_xml = lambda: data # type: ignore[method-assign] def _warn_incompatibility_with_xunit2( @@ -271,9 +275,7 @@ def _warn_incompatibility_with_xunit2( if xml is not None and xml.family not in ("xunit1", "legacy"): request.node.warn( PytestWarning( - "{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format( - fixture_name=fixture_name, family=xml.family - ) + f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')" ) ) @@ -354,7 +356,10 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object] record_testsuite_property("ARCH", "PPC") record_testsuite_property("STORAGE_TYPE", "CEPH") - ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. + :param name: + The property name. + :param value: + The property value. Will be converted to a string. .. warning:: @@ -362,17 +367,16 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object] `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See :issue:`7767` for details. """ - __tracebackhide__ = True def record_func(name: str, value: object) -> None: - """No-op function in case --junitxml was not passed in the command-line.""" + """No-op function in case --junit-xml was not passed in the command-line.""" __tracebackhide__ = True _check_record_param_type("name", name) xml = request.config.stash.get(xml_key, None) if xml is not None: - record_func = xml.add_global_property # noqa + record_func = xml.add_global_property return record_func @@ -386,7 +390,7 @@ def pytest_addoption(parser: Parser) -> None: metavar="path", type=functools.partial(filename_arg, optname="--junitxml"), default=None, - help="create junit-xml style report file at given path.", + help="Create junit-xml style report file at given path", ) group.addoption( "--junitprefix", @@ -394,7 +398,7 @@ def pytest_addoption(parser: Parser) -> None: action="store", metavar="str", default=None, - help="prepend prefix to classnames in junit-xml output", + help="Prepend prefix to classnames in junit-xml output", ) parser.addini( "junit_suite_name", "Test suite name for JUnit report", default="pytest" @@ -499,6 +503,10 @@ class LogXML: # Local hack to handle xdist report order. workernode = getattr(report, "node", None) reporter = self.node_reporters.pop((nodeid, workernode)) + + for propname, propvalue in report.user_properties: + reporter.add_property(propname, str(propvalue)) + if reporter is not None: reporter.finalize() @@ -596,9 +604,6 @@ class LogXML: reporter = self._opentestcase(report) reporter.write_captured_output(report) - for propname, propvalue in report.user_properties: - reporter.add_property(propname, str(propvalue)) - self.finalize(report) report_wid = getattr(report, "worker_id", None) report_ii = getattr(report, "item_index", None) @@ -620,7 +625,7 @@ class LogXML: def update_testcase_duration(self, report: TestReport) -> None: """Accumulate total duration for nodeid from given report and update the Junit.testcase with the new total if already created.""" - if self.report_duration == "total" or report.when == self.report_duration: + if self.report_duration in {"total", report.when}: reporter = self.node_reporter(report) reporter.duration += getattr(report, "duration", 0.0) @@ -642,8 +647,8 @@ class LogXML: def pytest_sessionfinish(self) -> None: dirname = os.path.dirname(os.path.abspath(self.logfile)) - if not os.path.isdir(dirname): - os.makedirs(dirname) + # exist_ok avoids filesystem race conditions between checking path existence and requesting creation + os.makedirs(dirname, exist_ok=True) with open(self.logfile, "w", encoding="utf-8") as logfile: suite_stop_time = timing.time() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/legacypath.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/legacypath.py index 37e8c24220e..d9de65b1a53 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/legacypath.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/legacypath.py @@ -1,17 +1,20 @@ +# mypy: allow-untyped-defs """Add backward compatibility support for the legacy py path type.""" + +import dataclasses +from pathlib import Path import shlex import subprocess -from pathlib import Path +from typing import Final +from typing import final from typing import List from typing import Optional from typing import TYPE_CHECKING from typing import Union -import attr from iniconfig import SectionWrapper from _pytest.cacheprovider import Cache -from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.config import Config @@ -31,9 +34,8 @@ from _pytest.pytester import RunResult from _pytest.terminal import TerminalReporter from _pytest.tmpdir import TempPathFactory -if TYPE_CHECKING: - from typing_extensions import Final +if TYPE_CHECKING: import pexpect @@ -89,7 +91,6 @@ class Testdir: return self._pytester.chdir() def finalize(self) -> None: - """See :meth:`Pytester._finalize`.""" return self._pytester._finalize() def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: @@ -268,10 +269,17 @@ class LegacyTestdirPlugin: @final -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class TempdirFactory: - """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH`` - for :class:``TempPathFactory``.""" + """Backward compatibility wrapper that implements ``py.path.local`` + for :class:`TempPathFactory`. + + .. note:: + These days, it is preferred to use ``tmp_path_factory``. + + :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. + + """ _tmppath_factory: TempPathFactory @@ -282,11 +290,11 @@ class TempdirFactory: self._tmppath_factory = tmppath_factory def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: - """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object.""" + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self) -> LEGACY_PATH: - """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" + """Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object.""" return legacy_path(self._tmppath_factory.getbasetemp().resolve()) @@ -307,11 +315,16 @@ class LegacyTmpdirPlugin: By default, a new base temporary directory is created each test session, and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a `legacy_path`_ object. + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html """ return legacy_path(tmp_path) @@ -371,7 +384,7 @@ def Config_inifile(self: Config) -> Optional[LEGACY_PATH]: return legacy_path(str(self.inipath)) if self.inipath else None -def Session_stardir(self: Session) -> LEGACY_PATH: +def Session_startdir(self: Session) -> LEGACY_PATH: """The path from which pytest was invoked. Prefer to use ``startpath`` which is a :class:`pathlib.Path`. @@ -426,7 +439,7 @@ def pytest_load_initial_conftests(early_config: Config) -> None: mp.setattr(Config, "inifile", property(Config_inifile), raising=False) # Add Session.startdir property. - mp.setattr(Session, "startdir", property(Session_stardir), raising=False) + mp.setattr(Session, "startdir", property(Session_startdir), raising=False) # Add pathlist configuration type. mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/logging.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/logging.py index 31ad8301076..af5e443ced1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/logging.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/logging.py @@ -1,26 +1,37 @@ +# mypy: allow-untyped-defs """Access and control log capturing.""" -import logging -import os -import re -import sys + from contextlib import contextmanager +from contextlib import nullcontext +from datetime import datetime +from datetime import timedelta +from datetime import timezone +import io from io import StringIO +import logging +from logging import LogRecord +import os from pathlib import Path +import re +from types import TracebackType from typing import AbstractSet from typing import Dict +from typing import final from typing import Generator +from typing import Generic from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING from typing import TypeVar from typing import Union from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager -from _pytest.compat import final -from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer @@ -35,6 +46,11 @@ from _pytest.stash import StashKey from _pytest.terminal import TerminalReporter +if TYPE_CHECKING: + logging_StreamHandler = logging.StreamHandler[StringIO] +else: + logging_StreamHandler = logging.StreamHandler + DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") @@ -46,7 +62,26 @@ def _remove_ansi_escape_sequences(text: str) -> str: return _ANSI_ESCAPE_SEQ.sub("", text) -class ColoredLevelFormatter(logging.Formatter): +class DatetimeFormatter(logging.Formatter): + """A logging formatter which formats record with + :func:`datetime.datetime.strftime` formatter instead of + :func:`time.strftime` in case of microseconds in format string. + """ + + def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: + if datefmt and "%f" in datefmt: + ct = self.converter(record.created) + tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) + # Construct `datetime.datetime` object from `struct_time` + # and msecs information from `record` + # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861). + dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz) + return dt.strftime(datefmt) + # Use `logging.Formatter` for non-microsecond formats + return super().formatTime(record, datefmt) + + +class ColoredLevelFormatter(DatetimeFormatter): """A logging formatter which colorizes the %(levelname)..s part of the log format passed to __init__.""" @@ -83,7 +118,6 @@ class ColoredLevelFormatter(logging.Formatter): .. warning:: This is an experimental API. """ - assert self._fmt is not None levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) if not levelname_fmt_match: @@ -150,7 +184,6 @@ class PercentStyleMultiline(logging.PercentStyle): 0 (auto-indent turned off) or >0 (explicitly set indentation position). """ - if auto_indent_option is None: return 0 elif isinstance(auto_indent_option, bool): @@ -177,7 +210,7 @@ class PercentStyleMultiline(logging.PercentStyle): if "\n" in record.message: if hasattr(record, "auto_indent"): # Passed in from the "extra={}" kwarg on the call to logging.log(). - auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined] + auto_indent = self._get_auto_indent(record.auto_indent) else: auto_indent = self._auto_indent @@ -212,7 +245,7 @@ def pytest_addoption(parser: Parser) -> None: def add_option_ini(option, dest, default=None, type=None, **kwargs): parser.addini( - dest, default=default, type=type, help="default value for " + option + dest, default=default, type=type, help="Default value for " + option ) group.addoption(option, dest=dest, **kwargs) @@ -222,8 +255,8 @@ def pytest_addoption(parser: Parser) -> None: default=None, metavar="LEVEL", help=( - "level of messages to catch/display.\n" - "Not set by default, so it depends on the root/parent log handler's" + "Level of messages to catch/display." + " Not set by default, so it depends on the root/parent log handler's" ' effective level, where it is "WARNING" by default.' ), ) @@ -231,58 +264,65 @@ def pytest_addoption(parser: Parser) -> None: "--log-format", dest="log_format", default=DEFAULT_LOG_FORMAT, - help="log format as used by the logging module.", + help="Log format used by the logging module", ) add_option_ini( "--log-date-format", dest="log_date_format", default=DEFAULT_LOG_DATE_FORMAT, - help="log date format as used by the logging module.", + help="Log date format used by the logging module", ) parser.addini( "log_cli", default=False, type="bool", - help='enable log display during test run (also known as "live logging").', + help='Enable log display during test run (also known as "live logging")', ) add_option_ini( - "--log-cli-level", dest="log_cli_level", default=None, help="cli logging level." + "--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level" ) add_option_ini( "--log-cli-format", dest="log_cli_format", default=None, - help="log format as used by the logging module.", + help="Log format used by the logging module", ) add_option_ini( "--log-cli-date-format", dest="log_cli_date_format", default=None, - help="log date format as used by the logging module.", + help="Log date format used by the logging module", ) add_option_ini( "--log-file", dest="log_file", default=None, - help="path to a file when logging will be written to.", + help="Path to a file when logging will be written to", + ) + add_option_ini( + "--log-file-mode", + dest="log_file_mode", + default="w", + choices=["w", "a"], + help="Log file open mode", ) add_option_ini( "--log-file-level", dest="log_file_level", default=None, - help="log file logging level.", + help="Log file logging level", ) add_option_ini( "--log-file-format", dest="log_file_format", - default=DEFAULT_LOG_FORMAT, - help="log format as used by the logging module.", + default=None, + help="Log format used by the logging module", ) add_option_ini( "--log-file-date-format", dest="log_file_date_format", - default=DEFAULT_LOG_DATE_FORMAT, - help="log date format as used by the logging module.", + default=None, + help="Log date format used by the logging module", ) add_option_ini( "--log-auto-indent", @@ -290,13 +330,20 @@ def pytest_addoption(parser: Parser) -> None: default=None, help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", ) + group.addoption( + "--log-disable", + action="append", + default=[], + dest="logger_disable", + help="Disable a logger by name. Can be passed multiple times.", + ) _HandlerType = TypeVar("_HandlerType", bound=logging.Handler) # Not using @contextmanager for performance reasons. -class catching_logs: +class catching_logs(Generic[_HandlerType]): """Context manager that prepares the whole logging machinery properly.""" __slots__ = ("handler", "level", "orig_level") @@ -305,7 +352,7 @@ class catching_logs: self.handler = handler self.level = level - def __enter__(self): + def __enter__(self) -> _HandlerType: root_logger = logging.getLogger() if self.level is not None: self.handler.setLevel(self.level) @@ -315,18 +362,21 @@ class catching_logs: root_logger.setLevel(min(self.orig_level, self.level)) return self.handler - def __exit__(self, type, value, traceback): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: root_logger = logging.getLogger() if self.level is not None: root_logger.setLevel(self.orig_level) root_logger.removeHandler(self.handler) -class LogCaptureHandler(logging.StreamHandler): +class LogCaptureHandler(logging_StreamHandler): """A logging handler that stores log records and the log text.""" - stream: StringIO - def __init__(self) -> None: """Create a new log handler.""" super().__init__(StringIO()) @@ -341,6 +391,10 @@ class LogCaptureHandler(logging.StreamHandler): self.records = [] self.stream = StringIO() + def clear(self) -> None: + self.records.clear() + self.stream = StringIO() + def handleError(self, record: logging.LogRecord) -> None: if logging.raiseExceptions: # Fail the test if the log message is bad (emit failed). @@ -360,11 +414,12 @@ class LogCaptureFixture: self._initial_handler_level: Optional[int] = None # Dict of log name -> log level. self._initial_logger_levels: Dict[Optional[str], int] = {} + self._initial_disabled_logging_level: Optional[int] = None def _finalize(self) -> None: """Finalize the fixture. - This restores the log levels changed by :meth:`set_level`. + This restores the log levels and the disabled logging levels changed by :meth:`set_level`. """ # Restore log levels. if self._initial_handler_level is not None: @@ -372,23 +427,26 @@ class LogCaptureFixture: for logger_name, level in self._initial_logger_levels.items(): logger = logging.getLogger(logger_name) logger.setLevel(level) + # Disable logging at the original disabled logging level. + if self._initial_disabled_logging_level is not None: + logging.disable(self._initial_disabled_logging_level) + self._initial_disabled_logging_level = None @property def handler(self) -> LogCaptureHandler: - """Get the logging handler used by the fixture. - - :rtype: LogCaptureHandler - """ + """Get the logging handler used by the fixture.""" return self._item.stash[caplog_handler_key] - def get_records(self, when: str) -> List[logging.LogRecord]: + def get_records( + self, when: Literal["setup", "call", "teardown"] + ) -> List[logging.LogRecord]: """Get the logging records for one of the possible test phases. - :param str when: - Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown". + :param when: + Which test phase to obtain the records from. + Valid values are: "setup", "call" and "teardown". :returns: The list of captured records at the given stage. - :rtype: List[logging.LogRecord] .. versionadded:: 3.4 """ @@ -436,17 +494,55 @@ class LogCaptureFixture: def clear(self) -> None: """Reset the list of log records and the captured log text.""" - self.handler.reset() + self.handler.clear() + + def _force_enable_logging( + self, level: Union[int, str], logger_obj: logging.Logger + ) -> int: + """Enable the desired logging level if the global level was disabled via ``logging.disabled``. + + Only enables logging levels greater than or equal to the requested ``level``. + + Does nothing if the desired ``level`` wasn't disabled. + + :param level: + The logger level caplog should capture. + All logging is enabled if a non-standard logging level string is supplied. + Valid level strings are in :data:`logging._nameToLevel`. + :param logger_obj: The logger object to check. + + :return: The original disabled logging level. + """ + original_disable_level: int = logger_obj.manager.disable + + if isinstance(level, str): + # Try to translate the level string to an int for `logging.disable()` + level = logging.getLevelName(level) + + if not isinstance(level, int): + # The level provided was not valid, so just un-disable all logging. + logging.disable(logging.NOTSET) + elif not logger_obj.isEnabledFor(level): + # Each level is `10` away from other levels. + # https://docs.python.org/3/library/logging.html#logging-levels + disable_level = max(level - 10, logging.NOTSET) + logging.disable(disable_level) + + return original_disable_level def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: - """Set the level of a logger for the duration of a test. + """Set the threshold level of a logger for the duration of a test. + + Logging messages which are less severe than this level will not be captured. .. versionchanged:: 3.4 The levels of the loggers changed by this function will be restored to their initial values at the end of the test. - :param int level: The level. - :param str logger: The logger to update. If not given, the root logger. + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. """ logger_obj = logging.getLogger(logger) # Save the original log-level to restore it during teardown. @@ -455,6 +551,9 @@ class LogCaptureFixture: if self._initial_handler_level is None: self._initial_handler_level = self.handler.level self.handler.setLevel(level) + initial_disabled_logging_level = self._force_enable_logging(level, logger_obj) + if self._initial_disabled_logging_level is None: + self._initial_disabled_logging_level = initial_disabled_logging_level @contextmanager def at_level( @@ -464,19 +563,39 @@ class LogCaptureFixture: the end of the 'with' statement the level is restored to its original value. - :param int level: The level. - :param str logger: The logger to update. If not given, the root logger. + Will enable the requested logging level if it was disabled via :func:`logging.disable`. + + :param level: The level. + :param logger: The logger to update. If not given, the root logger. """ logger_obj = logging.getLogger(logger) orig_level = logger_obj.level logger_obj.setLevel(level) handler_orig_level = self.handler.level self.handler.setLevel(level) + original_disable_level = self._force_enable_logging(level, logger_obj) try: yield finally: logger_obj.setLevel(orig_level) self.handler.setLevel(handler_orig_level) + logging.disable(original_disable_level) + + @contextmanager + def filtering(self, filter_: logging.Filter) -> Generator[None, None, None]: + """Context manager that temporarily adds the given filter to the caplog's + :meth:`handler` for the 'with' statement block, and removes that filter at the + end of the block. + + :param filter_: A custom :class:`logging.Filter` object. + + .. versionadded:: 7.5 + """ + self.handler.addFilter(filter_) + try: + yield + finally: + self.handler.removeFilter(filter_) @fixture @@ -513,9 +632,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i except ValueError as e: # Python logging does not recognise this as a logging level raise UsageError( - "'{}' is not recognized as a logging level name for " - "'{}'. Please consider passing the " - "logging level num instead.".format(log_level, setting_name) + f"'{log_level}' is not recognized as a logging level name for " + f"'{setting_name}'. Please consider passing the " + "logging level num instead." ) from e @@ -549,20 +668,25 @@ class LoggingPlugin: self.report_handler.setFormatter(self.formatter) # File logging. - self.log_file_level = get_log_level_for_setting(config, "log_file_level") + self.log_file_level = get_log_level_for_setting( + config, "log_file_level", "log_level" + ) log_file = get_option_ini(config, "log_file") or os.devnull if log_file != os.devnull: directory = os.path.dirname(os.path.abspath(log_file)) if not os.path.isdir(directory): os.makedirs(directory) - self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8") + self.log_file_mode = get_option_ini(config, "log_file_mode") or "w" + self.log_file_handler = _FileHandler( + log_file, mode=self.log_file_mode, encoding="UTF-8" + ) log_file_format = get_option_ini(config, "log_file_format", "log_format") log_file_date_format = get_option_ini( config, "log_file_date_format", "log_date_format" ) - log_file_formatter = logging.Formatter( + log_file_formatter = DatetimeFormatter( log_file_format, datefmt=log_file_date_format ) self.log_file_handler.setFormatter(log_file_formatter) @@ -573,6 +697,8 @@ class LoggingPlugin: ) if self._log_cli_enabled(): terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") + # Guaranteed by `_log_cli_enabled()`. + assert terminal_reporter is not None capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. self.log_cli_handler: Union[ @@ -586,6 +712,15 @@ class LoggingPlugin: get_option_ini(config, "log_auto_indent"), ) self.log_cli_handler.setFormatter(log_cli_formatter) + self._disable_loggers(loggers_to_disable=config.option.logger_disable) + + def _disable_loggers(self, loggers_to_disable: List[str]) -> None: + if not loggers_to_disable: + return + + for name in loggers_to_disable: + logger = logging.getLogger(name) + logger.disabled = True def _create_formatter(self, log_format, log_date_format, auto_indent): # Color option doesn't exist if terminal plugin is disabled. @@ -597,7 +732,7 @@ class LoggingPlugin: create_terminal_writer(self._config), log_format, log_date_format ) else: - formatter = logging.Formatter(log_format, log_date_format) + formatter = DatetimeFormatter(log_format, log_date_format) formatter._style = PercentStyleMultiline( formatter._style._fmt, auto_indent=auto_indent @@ -621,22 +756,13 @@ class LoggingPlugin: if not fpath.parent.exists(): fpath.parent.mkdir(exist_ok=True, parents=True) - stream = fpath.open(mode="w", encoding="UTF-8") - if sys.version_info >= (3, 7): - old_stream = self.log_file_handler.setStream(stream) - else: - old_stream = self.log_file_handler.stream - self.log_file_handler.acquire() - try: - self.log_file_handler.flush() - self.log_file_handler.stream = stream - finally: - self.log_file_handler.release() + # https://github.com/python/mypy/issues/11193 + stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment] + old_stream = self.log_file_handler.setStream(stream) if old_stream: - # https://github.com/python/typeshed/pull/5663 - old_stream.close() # type:ignore[attr-defined] + old_stream.close() - def _log_cli_enabled(self): + def _log_cli_enabled(self) -> bool: """Return whether live logging is enabled.""" enabled = self._config.getoption( "--log-cli-level" @@ -651,27 +777,26 @@ class LoggingPlugin: return True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionstart") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: if session.config.option.collectonly: - yield - return + return (yield) if self._log_cli_enabled() and self._config.getoption("verbose") < 1: # The verbose flag is needed to avoid messy test progress output. @@ -679,7 +804,7 @@ class LoggingPlugin: with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # Run all the tests. + return (yield) # Run all the tests. @hookimpl def pytest_runtest_logstart(self) -> None: @@ -704,12 +829,13 @@ class LoggingPlugin: item.stash[caplog_records_key][when] = caplog_handler.records item.stash[caplog_handler_key] = caplog_handler - yield - - log = report_handler.stream.getvalue().strip() - item.add_report_section(when, "log", log) + try: + yield + finally: + log = report_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") @@ -717,31 +843,33 @@ class LoggingPlugin: item.stash[caplog_records_key] = empty yield from self._runtest_for(item, "setup") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("teardown") - yield from self._runtest_for(item, "teardown") - del item.stash[caplog_records_key] - del item.stash[caplog_handler_key] + try: + yield from self._runtest_for(item, "teardown") + finally: + del item.stash[caplog_records_key] + del item.stash[caplog_handler_key] @hookimpl def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionfinish(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionfinish") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) @hookimpl def pytest_unconfigure(self) -> None: @@ -758,7 +886,7 @@ class _FileHandler(logging.FileHandler): pass -class _LiveLoggingStreamHandler(logging.StreamHandler): +class _LiveLoggingStreamHandler(logging_StreamHandler): """A logging StreamHandler used by the live logging feature: it will write a newline before the first log message in each test. diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/main.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/main.py index fea8179ca7d..716d5cf783b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/main.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/main.py @@ -1,30 +1,35 @@ """Core implementation of the testing process: init, session, runtest loop.""" + import argparse +import dataclasses import fnmatch import functools import importlib +import importlib.util import os -import sys from pathlib import Path +import sys +from typing import AbstractSet from typing import Callable from typing import Dict +from typing import final from typing import FrozenSet +from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import Optional from typing import overload from typing import Sequence -from typing import Set from typing import Tuple -from typing import Type from typing import TYPE_CHECKING from typing import Union +import warnings -import attr +import pluggy -import _pytest._code from _pytest import nodes -from _pytest.compat import final +import _pytest._code from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode @@ -32,26 +37,29 @@ from _pytest.config import hookimpl from _pytest.config import PytestPluginManager from _pytest.config import UsageError from _pytest.config.argparsing import Parser +from _pytest.config.compat import PathAwareHookProxy from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath from _pytest.pathlib import fnmatch_ex -from _pytest.pathlib import visit +from _pytest.pathlib import safe_exists +from _pytest.pathlib import scandir from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.runner import collect_one_node from _pytest.runner import SetupState +from _pytest.warning_types import PytestWarning if TYPE_CHECKING: - from typing_extensions import Literal + from typing import Self def pytest_addoption(parser: Parser) -> None: parser.addini( "norecursedirs", - "directory patterns to avoid for recursion", + "Directory patterns to avoid for recursion", type="args", default=[ "*.egg", @@ -67,26 +75,26 @@ def pytest_addoption(parser: Parser) -> None: ) parser.addini( "testpaths", - "directories to search for tests when no files or directories are given in the " - "command line.", + "Directories to search for tests when no files or directories are given on the " + "command line", type="args", default=[], ) - group = parser.getgroup("general", "running and selection options") + group = parser.getgroup("general", "Running and selection options") group._addoption( "-x", "--exitfirst", action="store_const", dest="maxfail", const=1, - help="exit instantly on first error or failed test.", + help="Exit instantly on first error or failed test", ) group = parser.getgroup("pytest-warnings") group.addoption( "-W", "--pythonwarnings", action="append", - help="set which warnings to report, see -W option of python itself.", + help="Set which warnings to report, see -W option of Python itself", ) parser.addini( "filterwarnings", @@ -102,37 +110,40 @@ def pytest_addoption(parser: Parser) -> None: type=int, dest="maxfail", default=0, - help="exit after first num failures or errors.", + help="Exit after first num failures or errors", ) group._addoption( "--strict-config", action="store_true", - help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.", + help="Any warnings encountered while parsing the `pytest` section of the " + "configuration file raise errors", ) group._addoption( "--strict-markers", action="store_true", - help="markers not registered in the `markers` section of the configuration file raise errors.", + help="Markers not registered in the `markers` section of the configuration " + "file raise errors", ) group._addoption( "--strict", action="store_true", - help="(deprecated) alias to --strict-markers.", + help="(Deprecated) alias to --strict-markers", ) group._addoption( "-c", - metavar="file", + "--config-file", + metavar="FILE", type=str, dest="inifilename", - help="load configuration from `file` instead of trying to locate one of the implicit " - "configuration files.", + help="Load configuration from `FILE` instead of trying to locate one of the " + "implicit configuration files.", ) group._addoption( "--continue-on-collection-errors", action="store_true", default=False, dest="continue_on_collection_errors", - help="Force test execution even if collection errors occur.", + help="Force test execution even if collection errors occur", ) group._addoption( "--rootdir", @@ -149,30 +160,30 @@ def pytest_addoption(parser: Parser) -> None: "--collect-only", "--co", action="store_true", - help="only collect tests, don't execute them.", + help="Only collect tests, don't execute them", ) group.addoption( "--pyargs", action="store_true", - help="try to interpret all arguments as python packages.", + help="Try to interpret all arguments as Python packages", ) group.addoption( "--ignore", action="append", metavar="path", - help="ignore path during collection (multi-allowed).", + help="Ignore path during collection (multi-allowed)", ) group.addoption( "--ignore-glob", action="append", metavar="path", - help="ignore path pattern during collection (multi-allowed).", + help="Ignore path pattern during collection (multi-allowed)", ) group.addoption( "--deselect", action="append", metavar="nodeid_prefix", - help="deselect item (via node id prefix) during collection (multi-allowed).", + help="Deselect item (via node id prefix) during collection (multi-allowed)", ) group.addoption( "--confcutdir", @@ -180,14 +191,14 @@ def pytest_addoption(parser: Parser) -> None: default=None, metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"), - help="only load conftest.py's relative to specified dir.", + help="Only load conftest.py's relative to specified dir", ) group.addoption( "--noconftest", action="store_true", dest="noconftest", default=False, - help="Don't load any conftest.py files.", + help="Don't load any conftest.py files", ) group.addoption( "--keepduplicates", @@ -195,7 +206,7 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", dest="keepduplicates", default=False, - help="Keep duplicate tests.", + help="Keep duplicate tests", ) group.addoption( "--collect-in-virtualenv", @@ -209,8 +220,14 @@ def pytest_addoption(parser: Parser) -> None: default="prepend", choices=["prepend", "append", "importlib"], dest="importmode", - help="prepend/append to sys.path when importing test modules and conftest files, " - "default is to prepend.", + help="Prepend/append to sys.path when importing test modules and conftest " + "files. Default: prepend.", + ) + parser.addini( + "consider_namespace_packages", + type="bool", + default=False, + help="Consider namespace packages when resolving module names during import", ) group = parser.getgroup("debugconfig", "test session debugging and configuration") @@ -221,8 +238,8 @@ def pytest_addoption(parser: Parser) -> None: type=validate_basetemp, metavar="dir", help=( - "base temporary directory for this test run." - "(warning: this directory is removed if it exists)" + "Base temporary directory for this test run. " + "(Warning: this directory is removed if it exists.)" ), ) @@ -373,8 +390,11 @@ def _in_venv(path: Path) -> bool: def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]: + if collection_path.name == "__pycache__": + return True + ignore_paths = config._getconftest_pathlist( - "collect_ignore", path=collection_path.parent, rootpath=config.rootpath + "collect_ignore", path=collection_path.parent ) ignore_paths = ignore_paths or [] excludeopt = config.getoption("ignore") @@ -385,7 +405,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo return True ignore_globs = config._getconftest_pathlist( - "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath + "collect_ignore_glob", path=collection_path.parent ) ignore_globs = ignore_globs or [] excludeglobopt = config.getoption("ignore_glob") @@ -398,9 +418,21 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo allow_in_venv = config.getoption("collect_in_virtualenv") if not allow_in_venv and _in_venv(collection_path): return True + + if collection_path.is_dir(): + norecursepatterns = config.getini("norecursedirs") + if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns): + return True + return None +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> Optional[nodes.Collector]: + return Dir.from_parent(parent, path=path) + + def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None: deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: @@ -420,11 +452,15 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No class FSHookProxy: - def __init__(self, pm: PytestPluginManager, remove_mods) -> None: + def __init__( + self, + pm: PytestPluginManager, + remove_mods: AbstractSet[object], + ) -> None: self.pm = pm self.remove_mods = remove_mods - def __getattr__(self, name: str): + def __getattr__(self, name: str) -> pluggy.HookCaller: x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x @@ -440,8 +476,10 @@ class Failed(Exception): """Signals a stop as failed test run.""" -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class _bestrelpath_cache(Dict[Path, str]): + __slots__ = ("path",) + path: Path def __missing__(self, path: Path) -> str: @@ -451,7 +489,63 @@ class _bestrelpath_cache(Dict[Path, str]): @final -class Session(nodes.FSCollector): +class Dir(nodes.Directory): + """Collector of files in a file system directory. + + .. versionadded:: 8.0 + + .. note:: + + Python directories with an `__init__.py` file are instead collected by + :class:`~pytest.Package` by default. Both are :class:`~pytest.Directory` + collectors. + """ + + @classmethod + def from_parent( # type: ignore[override] + cls, + parent: nodes.Collector, + *, + path: Path, + ) -> "Self": + """The public constructor. + + :param parent: The parent collector of this Dir. + :param path: The directory's path. + """ + return super().from_parent(parent=parent, path=path) + + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + config = self.config + col: Optional[nodes.Collector] + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols + + +@final +class Session(nodes.Collector): + """The root of the collection tree. + + ``Session`` collects the initial paths given as arguments to pytest. + """ + Interrupted = Interrupted Failed = Failed # Set on the session by runner.pytest_sessionstart. @@ -462,6 +556,7 @@ class Session(nodes.FSCollector): def __init__(self, config: Config) -> None: super().__init__( + name="", path=config.rootpath, fspath=None, parent=None, @@ -471,10 +566,15 @@ class Session(nodes.FSCollector): ) self.testsfailed = 0 self.testscollected = 0 - self.shouldstop: Union[bool, str] = False - self.shouldfail: Union[bool, str] = False + self._shouldstop: Union[bool, str] = False + self._shouldfail: Union[bool, str] = False self.trace = config.trace.root.get("collection") self._initialpaths: FrozenSet[Path] = frozenset() + self._initialpaths_with_parents: FrozenSet[Path] = frozenset() + self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] + self._initial_parts: List[CollectionArgument] = [] + self._collection_cache: Dict[nodes.Collector, CollectReport] = {} + self.items: List[nodes.Item] = [] self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) @@ -495,6 +595,42 @@ class Session(nodes.FSCollector): ) @property + def shouldstop(self) -> Union[bool, str]: + return self._shouldstop + + @shouldstop.setter + def shouldstop(self, value: Union[bool, str]) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldstop: + warnings.warn( + PytestWarning( + "session.shouldstop cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldstop = value + + @property + def shouldfail(self) -> Union[bool, str]: + return self._shouldfail + + @shouldfail.setter + def shouldfail(self, value: Union[bool, str]) -> None: + # The runner checks shouldfail and assumes that if it is set we are + # definitely stopping, so prevent unsetting it. + if value is False and self._shouldfail: + warnings.warn( + PytestWarning( + "session.shouldfail cannot be unset after it has been set; ignoring." + ), + stacklevel=2, + ) + return + self._shouldfail = value + + @property def startpath(self) -> Path: """The path from which pytest was invoked. @@ -525,80 +661,87 @@ class Session(nodes.FSCollector): pytest_collectreport = pytest_runtest_logreport - def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: + def isinitpath( + self, + path: Union[str, "os.PathLike[str]"], + *, + with_parents: bool = False, + ) -> bool: + """Is path an initial path? + + An initial path is a path explicitly given to pytest on the command + line. + + :param with_parents: + If set, also return True if the path is a parent of an initial path. + + .. versionchanged:: 8.0 + Added the ``with_parents`` parameter. + """ # Optimization: Path(Path(...)) is much slower than isinstance. path_ = path if isinstance(path, Path) else Path(path) - return path_ in self._initialpaths + if with_parents: + return path_ in self._initialpaths_with_parents + else: + return path_ in self._initialpaths - def gethookproxy(self, fspath: "os.PathLike[str]"): + def gethookproxy(self, fspath: "os.PathLike[str]") -> pluggy.HookRelay: # Optimization: Path(Path(...)) is much slower than isinstance. path = fspath if isinstance(fspath, Path) else Path(fspath) pm = self.config.pluginmanager # Check if we have the common case of running # hooks with all conftest.py files. - my_conftestmodules = pm._getconftestmodules( - path, - self.config.getoption("importmode"), - rootpath=self.config.rootpath, - ) + my_conftestmodules = pm._getconftestmodules(path) remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + proxy: pluggy.HookRelay if remove_mods: - # One or more conftests are not in use at this fspath. - from .config.compat import PathAwareHookProxy - - proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) + # One or more conftests are not in use at this path. + proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment] else: # All plugins are active for this fspath. proxy = self.config.hook return proxy - def _recurse(self, direntry: "os.DirEntry[str]") -> bool: - if direntry.name == "__pycache__": - return False - fspath = Path(direntry.path) - ihook = self.gethookproxy(fspath.parent) - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return False - norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False - return True - - def _collectfile( - self, fspath: Path, handle_dupes: bool = True + def _collect_path( + self, + path: Path, + path_cache: Dict[Path, Sequence[nodes.Collector]], ) -> Sequence[nodes.Collector]: - assert ( - fspath.is_file() - ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() - ) - ihook = self.gethookproxy(fspath) - if not self.isinitpath(fspath): - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return () + """Create a Collector for the given path. - if handle_dupes: - keepduplicates = self.config.getoption("keepduplicates") - if not keepduplicates: - duplicate_paths = self.config.pluginmanager._duplicatepaths - if fspath in duplicate_paths: - return () - else: - duplicate_paths.add(fspath) + `path_cache` makes it so the same Collectors are returned for the same + path. + """ + if path in path_cache: + return path_cache[path] + + if path.is_dir(): + ihook = self.gethookproxy(path.parent) + col: Optional[nodes.Collector] = ihook.pytest_collect_directory( + path=path, parent=self + ) + cols: Sequence[nodes.Collector] = (col,) if col is not None else () + + elif path.is_file(): + ihook = self.gethookproxy(path) + cols = ihook.pytest_collect_file(file_path=path, parent=self) + + else: + # Broken symlink or invalid/missing file. + cols = () - return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return] + path_cache[path] = cols + return cols @overload def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... - ) -> Sequence[nodes.Item]: - ... + ) -> Sequence[nodes.Item]: ... @overload def perform_collect( self, args: Optional[Sequence[str]] = ..., genitems: bool = ... - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: - ... + ) -> Sequence[Union[nodes.Item, nodes.Collector]]: ... def perform_collect( self, args: Optional[Sequence[str]] = None, genitems: bool = True @@ -622,33 +765,44 @@ class Session(nodes.FSCollector): self.trace("perform_collect", self, args) self.trace.root.indent += 1 - self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] - self._initial_parts: List[Tuple[Path, List[str]]] = [] - self.items: List[nodes.Item] = [] - hook = self.config.hook + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} + self.items = [] items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items try: initialpaths: List[Path] = [] + initialpaths_with_parents: List[Path] = [] for arg in args: - fspath, parts = resolve_collection_argument( + collection_argument = resolve_collection_argument( self.config.invocation_params.dir, arg, as_pypath=self.config.option.pyargs, ) - self._initial_parts.append((fspath, parts)) - initialpaths.append(fspath) + self._initial_parts.append(collection_argument) + initialpaths.append(collection_argument.path) + initialpaths_with_parents.append(collection_argument.path) + initialpaths_with_parents.extend(collection_argument.path.parents) self._initialpaths = frozenset(initialpaths) + self._initialpaths_with_parents = frozenset(initialpaths_with_parents) + rep = collect_one_node(self) self.ihook.pytest_collectreport(report=rep) self.trace.root.indent -= 1 if self._notfound: errors = [] - for arg, cols in self._notfound: - line = f"(no name {arg!r} in any of {cols!r})" - errors.append(f"not found: {arg}\n{line}") + for arg, collectors in self._notfound: + if collectors: + errors.append( + f"not found: {arg}\n(no match in any of {collectors!r})" + ) + else: + errors.append(f"found no collectors for {arg}") + raise UsageError(*errors) + if not genitems: items = rep.result else: @@ -661,155 +815,147 @@ class Session(nodes.FSCollector): session=self, config=self.config, items=items ) finally: + self._notfound = [] + self._initial_parts = [] + self._collection_cache = {} hook.pytest_collection_finish(session=self) - self.testscollected = len(items) - return items + if genitems: + self.testscollected = len(items) - def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: - from _pytest.python import Package + return items - # Keep track of any collected nodes in here, so we don't duplicate fixtures. - node_cache1: Dict[Path, Sequence[nodes.Collector]] = {} - node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = {} + def _collect_one_node( + self, + node: nodes.Collector, + handle_dupes: bool = True, + ) -> Tuple[CollectReport, bool]: + if node in self._collection_cache and handle_dupes: + rep = self._collection_cache[node] + return rep, True + else: + rep = collect_one_node(node) + self._collection_cache[node] = rep + return rep, False - # Keep track of any collected collectors in matchnodes paths, so they - # are not collected more than once. - matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {} + def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: + # This is a cache for the root directories of the initial paths. + # We can't use collection_cache for Session because of its special + # role as the bootstrapping collector. + path_cache: Dict[Path, Sequence[nodes.Collector]] = {} - # Dirnames of pkgs with dunder-init files. - pkg_roots: Dict[str, Package] = {} + pm = self.config.pluginmanager - for argpath, names in self._initial_parts: - self.trace("processing argument", (argpath, names)) + for collection_argument in self._initial_parts: + self.trace("processing argument", collection_argument) self.trace.root.indent += 1 - # Start with a Session root, and delve to argpath item (dir or file) - # and stack all Packages found on the way. - # No point in finding packages when collecting doctests. - if not self.config.getoption("doctestmodules", False): - pm = self.config.pluginmanager - confcutdir = pm._confcutdir - for parent in (argpath, *argpath.parents): - if confcutdir and parent in confcutdir.parents: - break + argpath = collection_argument.path + names = collection_argument.parts + module_name = collection_argument.module_name - if parent.is_dir(): - pkginit = parent / "__init__.py" - if pkginit.is_file() and pkginit not in node_cache1: - col = self._collectfile(pkginit, handle_dupes=False) - if col: - if isinstance(col[0], Package): - pkg_roots[str(parent)] = col[0] - node_cache1[col[0].path] = [col[0]] - - # If it's a directory argument, recurse and look for any Subpackages. - # Let the Package collector deal with subnodes, don't collect here. + # resolve_collection_argument() ensures this. if argpath.is_dir(): assert not names, f"invalid arg {(argpath, names)!r}" - seen_dirs: Set[Path] = set() - for direntry in visit(str(argpath), self._recurse): - if not direntry.is_file(): - continue - - path = Path(direntry.path) - dirpath = path.parent - - if dirpath not in seen_dirs: - # Collect packages first. - seen_dirs.add(dirpath) - pkginit = dirpath / "__init__.py" - if pkginit.exists(): - for x in self._collectfile(pkginit): - yield x - if isinstance(x, Package): - pkg_roots[str(dirpath)] = x - if str(dirpath) in pkg_roots: - # Do not collect packages here. - continue - - for x in self._collectfile(path): - key2 = (type(x), x.path) - if key2 in node_cache2: - yield node_cache2[key2] - else: - node_cache2[key2] = x - yield x + paths = [argpath] + # Add relevant parents of the path, from the root, e.g. + # /a/b/c.py -> [/, /a, /a/b, /a/b/c.py] + if module_name is None: + # Paths outside of the confcutdir should not be considered. + for path in argpath.parents: + if not pm._is_in_confcutdir(path): + break + paths.insert(0, path) else: - assert argpath.is_file() - - if argpath in node_cache1: - col = node_cache1[argpath] - else: - collect_root = pkg_roots.get(str(argpath.parent), self) - col = collect_root._collectfile(argpath, handle_dupes=False) - if col: - node_cache1[argpath] = col - - matching = [] - work: List[ - Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]] - ] = [(col, names)] - while work: - self.trace("matchnodes", col, names) - self.trace.root.indent += 1 - - matchnodes, matchnames = work.pop() - for node in matchnodes: - if not matchnames: - matching.append(node) - continue - if not isinstance(node, nodes.Collector): - continue - key = (type(node), node.nodeid) - if key in matchnodes_cache: - rep = matchnodes_cache[key] - else: - rep = collect_one_node(node) - matchnodes_cache[key] = rep - if rep.passed: - submatchnodes = [] - for r in rep.result: - # TODO: Remove parametrized workaround once collection structure contains - # parametrization. - if ( - r.name == matchnames[0] - or r.name.split("[")[0] == matchnames[0] - ): - submatchnodes.append(r) - if submatchnodes: - work.append((submatchnodes, matchnames[1:])) - else: - # Report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134). - node.ihook.pytest_collectreport(report=rep) - - self.trace("matchnodes finished -> ", len(matching), "nodes") - self.trace.root.indent -= 1 - - if not matching: - report_arg = "::".join((str(argpath), *names)) - self._notfound.append((report_arg, col)) + # For --pyargs arguments, only consider paths matching the module + # name. Paths beyond the package hierarchy are not included. + module_name_parts = module_name.split(".") + for i, path in enumerate(argpath.parents, 2): + if i > len(module_name_parts) or path.stem != module_name_parts[-i]: + break + paths.insert(0, path) + + # Start going over the parts from the root, collecting each level + # and discarding all nodes which don't match the level's part. + any_matched_in_initial_part = False + notfound_collectors = [] + work: List[ + Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]] + ] = [(self, [*paths, *names])] + while work: + matchnode, matchparts = work.pop() + + # Pop'd all of the parts, this is a match. + if not matchparts: + yield matchnode + any_matched_in_initial_part = True continue - # If __init__.py was the only file requested, then the matched - # node will be the corresponding Package (by default), and the - # first yielded item will be the __init__ Module itself, so - # just use that. If this special case isn't taken, then all the - # files in the package will be yielded. - if argpath.name == "__init__.py" and isinstance(matching[0], Package): - try: - yield next(iter(matching[0].collect())) - except StopIteration: - # The package collects nothing with only an __init__.py - # file in it, which gets ignored by the default - # "python_files" option. - pass + # Should have been matched by now, discard. + if not isinstance(matchnode, nodes.Collector): continue - yield from matching + # Collect this level of matching. + # Collecting Session (self) is done directly to avoid endless + # recursion to this function. + subnodes: Sequence[Union[nodes.Collector, nodes.Item]] + if isinstance(matchnode, Session): + assert isinstance(matchparts[0], Path) + subnodes = matchnode._collect_path(matchparts[0], path_cache) + else: + # For backward compat, files given directly multiple + # times on the command line should not be deduplicated. + handle_dupes = not ( + len(matchparts) == 1 + and isinstance(matchparts[0], Path) + and matchparts[0].is_file() + ) + rep, duplicate = self._collect_one_node(matchnode, handle_dupes) + if not duplicate and not rep.passed: + # Report collection failures here to avoid failing to + # run some test specified in the command line because + # the module could not be imported (#134). + matchnode.ihook.pytest_collectreport(report=rep) + if not rep.passed: + continue + subnodes = rep.result + + # Prune this level. + any_matched_in_collector = False + for node in reversed(subnodes): + # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`. + if isinstance(matchparts[0], Path): + is_match = node.path == matchparts[0] + if sys.platform == "win32" and not is_match: + # In case the file paths do not match, fallback to samefile() to + # account for short-paths on Windows (#11895). + same_file = os.path.samefile(node.path, matchparts[0]) + # We don't want to match links to the current node, + # otherwise we would match the same file more than once (#12039). + is_match = same_file and ( + os.path.islink(node.path) + == os.path.islink(matchparts[0]) + ) + + # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. + else: + # TODO: Remove parametrized workaround once collection structure contains + # parametrization. + is_match = ( + node.name == matchparts[0] + or node.name.split("[")[0] == matchparts[0] + ) + if is_match: + work.append((node, matchparts[1:])) + any_matched_in_collector = True + + if not any_matched_in_collector: + notfound_collectors.append(matchnode) + + if not any_matched_in_initial_part: + report_arg = "::".join((str(argpath), *names)) + self._notfound.append((report_arg, notfound_collectors)) self.trace.root.indent -= 1 @@ -822,33 +968,49 @@ class Session(nodes.FSCollector): yield node else: assert isinstance(node, nodes.Collector) - rep = collect_one_node(node) + keepduplicates = self.config.getoption("keepduplicates") + # For backward compat, dedup only applies to files. + handle_dupes = not (keepduplicates and isinstance(node, nodes.File)) + rep, duplicate = self._collect_one_node(node, handle_dupes) + if duplicate and not keepduplicates: + return if rep.passed: for subnode in rep.result: yield from self.genitems(subnode) - node.ihook.pytest_collectreport(report=rep) + if not duplicate: + node.ihook.pytest_collectreport(report=rep) -def search_pypath(module_name: str) -> str: - """Search sys.path for the given a dotted module name, and return its file system path.""" +def search_pypath(module_name: str) -> Optional[str]: + """Search sys.path for the given a dotted module name, and return its file + system path if found.""" try: spec = importlib.util.find_spec(module_name) # AttributeError: looks like package module, but actually filename # ImportError: module does not exist # ValueError: not a module name except (AttributeError, ImportError, ValueError): - return module_name + return None if spec is None or spec.origin is None or spec.origin == "namespace": - return module_name + return None elif spec.submodule_search_locations: return os.path.dirname(spec.origin) else: return spec.origin +@dataclasses.dataclass(frozen=True) +class CollectionArgument: + """A resolved collection argument.""" + + path: Path + parts: Sequence[str] + module_name: Optional[str] + + def resolve_collection_argument( invocation_path: Path, arg: str, *, as_pypath: bool = False -) -> Tuple[Path, List[str]]: +) -> CollectionArgument: """Parse path arguments optionally containing selection parts and return (fspath, names). Command-line arguments can point to files and/or directories, and optionally contain @@ -856,9 +1018,13 @@ def resolve_collection_argument( "pkg/tests/test_foo.py::TestClass::test_foo" - This function ensures the path exists, and returns a tuple: + This function ensures the path exists, and returns a resolved `CollectionArgument`: - (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"]) + CollectionArgument( + path=Path("/full/path/to/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + module_name=None, + ) When as_pypath is True, expects that the command-line argument actually contains module paths instead of file-system paths: @@ -866,7 +1032,13 @@ def resolve_collection_argument( "pkg.tests.test_foo::TestClass::test_foo" In which case we search sys.path for a matching module, and then return the *path* to the - found module. + found module, which may look like this: + + CollectionArgument( + path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"), + parts=["TestClass", "test_foo"], + module_name="pkg.tests.test_foo", + ) If the path doesn't exist, raise UsageError. If the path is a directory and selection parts are present, raise UsageError. @@ -875,11 +1047,15 @@ def resolve_collection_argument( strpath, *parts = base.split("::") if parts: parts[-1] = f"{parts[-1]}{squacket}{rest}" + module_name = None if as_pypath: - strpath = search_pypath(strpath) + pyarg_strpath = search_pypath(strpath) + if pyarg_strpath is not None: + module_name = strpath + strpath = pyarg_strpath fspath = invocation_path / strpath fspath = absolutepath(fspath) - if not fspath.exists(): + if not safe_exists(fspath): msg = ( "module or package not found: {arg} (missing __init__.py?)" if as_pypath @@ -893,4 +1069,8 @@ def resolve_collection_argument( else "directory argument cannot contain :: selection parts: {arg}" ) raise UsageError(msg.format(arg=arg)) - return fspath, parts + return CollectionArgument( + path=fspath, + parts=parts, + module_name=module_name, + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py index 7e082f2e6e0..01d6e7165f2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/__init__.py @@ -1,5 +1,6 @@ """Generic mechanism for marking and selecting python functions.""" -import warnings + +import dataclasses from typing import AbstractSet from typing import Collection from typing import List @@ -7,8 +8,6 @@ from typing import Optional from typing import TYPE_CHECKING from typing import Union -import attr - from .expression import Expression from .expression import ParseError from .structures import EMPTY_PARAMETERSET_OPTION @@ -23,10 +22,9 @@ from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.config.argparsing import Parser -from _pytest.deprecated import MINUS_K_COLON -from _pytest.deprecated import MINUS_K_DASH from _pytest.stash import StashKey + if TYPE_CHECKING: from _pytest.nodes import Item @@ -65,8 +63,8 @@ def param( assert eval(test_input) == expected :param values: Variable args of the values of the parameter set, in order. - :keyword marks: A single mark or a list of marks to be applied to this parameter set. - :keyword str id: The id to attribute to this parameter set. + :param marks: A single mark or a list of marks to be applied to this parameter set. + :param id: The id to attribute to this parameter set. """ return ParameterSet.param(*values, marks=marks, id=id) @@ -79,8 +77,8 @@ def pytest_addoption(parser: Parser) -> None: dest="keyword", default="", metavar="EXPRESSION", - help="only run tests which match the given substring expression. " - "An expression is a python evaluatable expression " + help="Only run tests which match the given substring expression. " + "An expression is a Python evaluable expression " "where all names are substring-matched against test names " "and their parent classes. Example: -k 'test_method or test_" "other' matches all test functions and classes whose name " @@ -99,7 +97,7 @@ def pytest_addoption(parser: Parser) -> None: dest="markexpr", default="", metavar="MARKEXPR", - help="only run tests matching given mark expression.\n" + help="Only run tests matching given mark expression. " "For example: -m 'mark1 and not mark2'.", ) @@ -109,8 +107,8 @@ def pytest_addoption(parser: Parser) -> None: help="show markers (builtin, plugin and per-project ones).", ) - parser.addini("markers", "markers for test functions", "linelist") - parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets") + parser.addini("markers", "Register new markers for test functions", "linelist") + parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets") @hookimpl(tryfirst=True) @@ -133,7 +131,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: return None -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class KeywordMatcher: """A matcher for keywords. @@ -148,18 +146,27 @@ class KeywordMatcher: any item, as well as names directly assigned to test functions. """ + __slots__ = ("_names",) + _names: AbstractSet[str] @classmethod def from_item(cls, item: "Item") -> "KeywordMatcher": mapped_names = set() - # Add the names of the current item and any parent items. + # Add the names of the current item and any parent items, + # except the Session and root Directory's which are not + # interesting for matching. import pytest for node in item.listchain(): - if not isinstance(node, pytest.Session): - mapped_names.add(node.name) + if isinstance(node, pytest.Session): + continue + if isinstance(node, pytest.Directory) and isinstance( + node.parent, pytest.Session + ): + continue + mapped_names.add(node.name) # Add the names added as extra keywords to current or parent items. mapped_names.update(item.listextrakeywords()) @@ -189,27 +196,14 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: if not keywordexpr: return - if keywordexpr.startswith("-"): - # To be removed in pytest 8.0.0. - warnings.warn(MINUS_K_DASH, stacklevel=2) - keywordexpr = "not " + keywordexpr[1:] - selectuntil = False - if keywordexpr[-1:] == ":": - # To be removed in pytest 8.0.0. - warnings.warn(MINUS_K_COLON, stacklevel=2) - selectuntil = True - keywordexpr = keywordexpr[:-1] - expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'") remaining = [] deselected = [] for colitem in items: - if keywordexpr and not expr.evaluate(KeywordMatcher.from_item(colitem)): + if not expr.evaluate(KeywordMatcher.from_item(colitem)): deselected.append(colitem) else: - if selectuntil: - keywordexpr = None remaining.append(colitem) if deselected: @@ -217,13 +211,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: items[:] = remaining -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class MarkMatcher: """A matcher for markers which are present. Tries to match on any marker names, attached to the given colitem. """ + __slots__ = ("own_mark_names",) + own_mark_names: AbstractSet[str] @classmethod @@ -273,8 +269,8 @@ def pytest_configure(config: Config) -> None: if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): raise UsageError( - "{!s} must be one of skip, xfail or fail_at_collect" - " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset) + f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect" + f" but it is {empty_parameterset!r}" ) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/expression.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/expression.py index 92220d7723a..78b7fda696b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/expression.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/expression.py @@ -14,21 +14,18 @@ The semantics are: - ident evaluates to True of False according to a provided matcher function. - or/and/not evaluate according to the usual boolean semantics. """ + import ast +import dataclasses import enum import re import types from typing import Callable from typing import Iterator from typing import Mapping +from typing import NoReturn from typing import Optional from typing import Sequence -from typing import TYPE_CHECKING - -import attr - -if TYPE_CHECKING: - from typing import NoReturn __all__ = [ @@ -47,8 +44,9 @@ class TokenType(enum.Enum): EOF = "end of input" -@attr.s(frozen=True, slots=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Token: + __slots__ = ("type", "value", "pos") type: TokenType value: str pos: int @@ -117,7 +115,7 @@ class Scanner: self.reject((type,)) return None - def reject(self, expected: Sequence[TokenType]) -> "NoReturn": + def reject(self, expected: Sequence[TokenType]) -> NoReturn: raise ParseError( self.current.pos + 1, "expected {}; got {}".format( @@ -135,7 +133,7 @@ IDENT_PREFIX = "$" def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret: ast.expr = ast.NameConstant(False) + ret: ast.expr = ast.Constant(False) else: ret = expr(s) s.accept(TokenType.EOF, reject=True) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/structures.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/structures.py index 0e42cd8de5f..a6503bf1d46 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/structures.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/mark/structures.py @@ -1,9 +1,11 @@ +# mypy: allow-untyped-defs import collections.abc +import dataclasses import inspect -import warnings from typing import Any from typing import Callable from typing import Collection +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -19,19 +21,19 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - -import attr +import warnings from .._code import getfslineno from ..compat import ascii_escaped -from ..compat import final from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config from _pytest.deprecated import check_ispytest +from _pytest.deprecated import MARKED_FIXTURE from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning + if TYPE_CHECKING: from ..nodes import Node @@ -72,16 +74,11 @@ def get_empty_parameterset_mark( return mark -class ParameterSet( - NamedTuple( - "ParameterSet", - [ - ("values", Sequence[Union[object, NotSetType]]), - ("marks", Collection[Union["MarkDecorator", "Mark"]]), - ("id", Optional[str]), - ], - ) -): +class ParameterSet(NamedTuple): + values: Sequence[Union[object, NotSetType]] + marks: Collection[Union["MarkDecorator", "Mark"]] + id: Optional[str] + @classmethod def param( cls, @@ -116,7 +113,6 @@ class ParameterSet( Enforce tuple wrapping so single argument tuple values don't get decomposed and break tests. """ - if isinstance(parameterset, cls): return parameterset if force_tuple: @@ -131,12 +127,12 @@ class ParameterSet( @staticmethod def _parse_parametrize_args( - argnames: Union[str, List[str], Tuple[str, ...]], + argnames: Union[str, Sequence[str]], argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], *args, **kwargs, - ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: - if not isinstance(argnames, (tuple, list)): + ) -> Tuple[Sequence[str], bool]: + if isinstance(argnames, str): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 else: @@ -155,12 +151,12 @@ class ParameterSet( @classmethod def _for_parametrize( cls, - argnames: Union[str, List[str], Tuple[str, ...]], + argnames: Union[str, Sequence[str]], argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], func, config: Config, nodeid: str, - ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: + ) -> Tuple[Sequence[str], List["ParameterSet"]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) del argvalues @@ -196,8 +192,10 @@ class ParameterSet( @final -@attr.s(frozen=True, init=False, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Mark: + """A pytest mark.""" + #: Name of the mark. name: str #: Positional arguments of the mark decorator. @@ -206,9 +204,11 @@ class Mark: kwargs: Mapping[str, Any] #: Source Mark for ids with parametrize Marks. - _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False) + _param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False) #: Resolved/generated ids with parametrize Marks. - _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False) + _param_ids_generated: Optional[Sequence[str]] = dataclasses.field( + default=None, repr=False + ) def __init__( self, @@ -266,14 +266,14 @@ class Mark: Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class MarkDecorator: """A decorator for applying a mark on test functions and classes. ``MarkDecorators`` are created with ``pytest.mark``:: - mark1 = pytest.mark.NAME # Simple MarkDecorator - mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator + mark1 = pytest.mark.NAME # Simple MarkDecorator + mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator and can then be applied as decorators to test functions:: @@ -342,7 +342,7 @@ class MarkDecorator: # return type. Not much we can do about that. Thankfully mypy picks # the first match so it works out even if we break the rules. @overload - def __call__(self, arg: Markable) -> Markable: # type: ignore[misc] + def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap] pass @overload @@ -355,21 +355,46 @@ class MarkDecorator: func = args[0] is_class = inspect.isclass(func) if len(args) == 1 and (istestfunc(func) or is_class): - store_mark(func, self.mark) + store_mark(func, self.mark, stacklevel=3) return func return self.with_args(*args, **kwargs) -def get_unpacked_marks(obj: object) -> Iterable[Mark]: - """Obtain the unpacked marks that are stored on an object.""" - mark_list = getattr(obj, "pytestmark", []) - if not isinstance(mark_list, list): - mark_list = [mark_list] - return normalize_mark_list(mark_list) +def get_unpacked_marks( + obj: Union[object, type], + *, + consider_mro: bool = True, +) -> List[Mark]: + """Obtain the unpacked marks that are stored on an object. + + If obj is a class and consider_mro is true, return marks applied to + this class and all of its super-classes in MRO order. If consider_mro + is false, only return marks applied directly to this class. + """ + if isinstance(obj, type): + if not consider_mro: + mark_lists = [obj.__dict__.get("pytestmark", [])] + else: + mark_lists = [ + x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) + ] + mark_list = [] + for item in mark_lists: + if isinstance(item, list): + mark_list.extend(item) + else: + mark_list.append(item) + else: + mark_attribute = getattr(obj, "pytestmark", []) + if isinstance(mark_attribute, list): + mark_list = mark_attribute + else: + mark_list = [mark_attribute] + return list(normalize_mark_list(mark_list)) def normalize_mark_list( - mark_list: Iterable[Union[Mark, MarkDecorator]] + mark_list: Iterable[Union[Mark, MarkDecorator]], ) -> Iterable[Mark]: """ Normalize an iterable of Mark or MarkDecorator objects into a list of marks @@ -381,19 +406,25 @@ def normalize_mark_list( for mark in mark_list: mark_obj = getattr(mark, "mark", mark) if not isinstance(mark_obj, Mark): - raise TypeError(f"got {repr(mark_obj)} instead of Mark") + raise TypeError(f"got {mark_obj!r} instead of Mark") yield mark_obj -def store_mark(obj, mark: Mark) -> None: +def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: """Store a Mark on an object. This is used to implement the Mark declarations/decorators correctly. """ assert isinstance(mark, Mark), mark + + from ..fixtures import getfixturemarker + + if getfixturemarker(obj) is not None: + warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel) + # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. - obj.pytestmark = [*get_unpacked_marks(obj), mark] + obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] # Typing for builtin pytest marks. This is cheating; it gives builtin marks @@ -402,13 +433,11 @@ if TYPE_CHECKING: from _pytest.scope import _ScopeName class _SkipMarkDecorator(MarkDecorator): - @overload # type: ignore[override,misc] - def __call__(self, arg: Markable) -> Markable: - ... + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... @overload - def __call__(self, reason: str = ...) -> "MarkDecorator": - ... + def __call__(self, reason: str = ...) -> "MarkDecorator": ... class _SkipifMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] @@ -416,30 +445,29 @@ if TYPE_CHECKING: condition: Union[str, bool] = ..., *conditions: Union[str, bool], reason: str = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _XfailMarkDecorator(MarkDecorator): - @overload # type: ignore[override,misc] - def __call__(self, arg: Markable) -> Markable: - ... + @overload # type: ignore[override,no-overload-impl] + def __call__(self, arg: Markable) -> Markable: ... @overload def __call__( self, - condition: Union[str, bool] = ..., + condition: Union[str, bool] = False, *conditions: Union[str, bool], reason: str = ..., run: bool = ..., - raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ..., + raises: Union[ + None, Type[BaseException], Tuple[Type[BaseException], ...] + ] = ..., strict: bool = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _ParametrizeMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] self, - argnames: Union[str, List[str], Tuple[str, ...]], + argnames: Union[str, Sequence[str]], argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], *, indirect: Union[bool, Sequence[str]] = ..., @@ -450,8 +478,7 @@ if TYPE_CHECKING: ] ] = ..., scope: Optional[_ScopeName] = ..., - ) -> MarkDecorator: - ... + ) -> MarkDecorator: ... class _UsefixturesMarkDecorator(MarkDecorator): def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override] @@ -471,9 +498,10 @@ class MarkGenerator: import pytest + @pytest.mark.slowtest def test_function(): - pass + pass applies a 'slowtest' :class:`Mark` on ``test_function``. """ diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py index 31f95a95ab2..3f398df76b1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/monkeypatch.py @@ -1,23 +1,27 @@ +# mypy: allow-untyped-defs """Monkeypatching and mocking functionality.""" + +from contextlib import contextmanager import os import re import sys -import warnings -from contextlib import contextmanager from typing import Any +from typing import final from typing import Generator from typing import List +from typing import Mapping from typing import MutableMapping from typing import Optional from typing import overload from typing import Tuple from typing import TypeVar from typing import Union +import warnings -from _pytest.compat import final from _pytest.fixtures import fixture from _pytest.warning_types import PytestWarning + RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") @@ -29,21 +33,26 @@ V = TypeVar("V") def monkeypatch() -> Generator["MonkeyPatch", None, None]: """A convenient fixture for monkey-patching. - The fixture provides these methods to modify objects, dictionaries or - os.environ:: + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>` + * :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>` + * :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>` + * :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>` + * :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>` + * :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>` + * :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>` + * :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>` + * :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>` All modifications will be undone after the requesting test function or - fixture has finished. The ``raising`` parameter determines if a KeyError - or AttributeError will be raised if the set/deletion operation has no target. + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. + + To undo modifications done by the fixture in a contained scope, + use :meth:`context() <pytest.MonkeyPatch.context>`. """ mpatch = MonkeyPatch() yield mpatch @@ -55,7 +64,7 @@ def resolve(name: str) -> object: parts = name.split(".") used = parts.pop(0) - found = __import__(used) + found: object = __import__(used) for part in parts: used += "." + part try: @@ -83,9 +92,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object: obj = getattr(obj, name) except AttributeError as e: raise AttributeError( - "{!r} object at {} has no attribute {!r}".format( - type(obj).__name__, ann, name - ) + f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}" ) from e return obj @@ -115,7 +122,7 @@ class MonkeyPatch: Returned by the :fixture:`monkeypatch` fixture. - :versionchanged:: 6.2 + .. versionchanged:: 6.2 Can now also be used directly as `pytest.MonkeyPatch()`, for when the fixture is not available. In this case, use :meth:`with MonkeyPatch.context() as mp: <context>` or remember to call @@ -124,7 +131,7 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = [] + self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -135,7 +142,6 @@ class MonkeyPatch: which undoes any patching done inside the ``with`` block upon exit. Example: - .. code-block:: python import functools @@ -162,8 +168,7 @@ class MonkeyPatch: name: object, value: Notset = ..., raising: bool = ..., - ) -> None: - ... + ) -> None: ... @overload def setattr( @@ -172,8 +177,7 @@ class MonkeyPatch: name: str, value: object, raising: bool = ..., - ) -> None: - ... + ) -> None: ... def setattr( self, @@ -182,16 +186,40 @@ class MonkeyPatch: value: object = notset, raising: bool = True, ) -> None: - """Set attribute value on target, memorizing the old value. + """ + Set attribute value on target, memorizing the old value. + + For example: - For convenience you can specify a string as ``target`` which + .. code-block:: python + + import os + + monkeypatch.setattr(os, "getcwd", lambda: "/") + + The code above replaces the :func:`os.getcwd` function by a ``lambda`` which + always returns ``"/"``. + + For convenience, you can specify a string as ``target`` which will be interpreted as a dotted import path, with the last part - being the attribute name. For example, - ``monkeypatch.setattr("os.getcwd", lambda: "/")`` - would set the ``getcwd`` function of the ``os`` module. + being the attribute name: + + .. code-block:: python - Raises AttributeError if the attribute does not exist, unless + monkeypatch.setattr("os.getcwd", lambda: "/") + + Raises :class:`AttributeError` if the attribute does not exist, unless ``raising`` is set to False. + + **Where to patch** + + ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one. + There can be many names pointing to any individual object, so for patching to work you must ensure + that you patch the name used by the system under test. + + See the section :ref:`Where to patch <python:where-to-patch>` in the :mod:`unittest.mock` + docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but + applies to ``monkeypatch.setattr`` as well. """ __tracebackhide__ = True import inspect @@ -261,12 +289,13 @@ class MonkeyPatch: self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: + def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: """Set dictionary entry ``name`` to value.""" self._setitem.append((dic, name, dic.get(name, notset))) - dic[name] = value + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dic[name] = value # type: ignore[index] - def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: + def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to @@ -277,7 +306,8 @@ class MonkeyPatch: raise KeyError(name) else: self._setitem.append((dic, name, dic.get(name, notset))) - del dic[name] + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dic[name] # type: ignore[attr-defined] def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """Set environment variable ``name`` to ``value``. @@ -289,10 +319,8 @@ class MonkeyPatch: if not isinstance(value, str): warnings.warn( # type: ignore[unreachable] PytestWarning( - "Value of environment variable {name} type should be str, but got " - "{value!r} (type: {type}); converted to str implicitly".format( - name=name, value=value, type=type(value).__name__ - ) + f"Value of environment variable {name} type should be str, but got " + f"{value!r} (type: {type(value).__name__}); converted to str implicitly" ), stacklevel=2, ) @@ -312,7 +340,6 @@ class MonkeyPatch: def syspath_prepend(self, path) -> None: """Prepend ``path`` to ``sys.path`` list of import locations.""" - if self._savesyspath is None: self._savesyspath = sys.path[:] sys.path.insert(0, str(path)) @@ -338,7 +365,8 @@ class MonkeyPatch: def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None: """Change the current working directory to the specified path. - Path can be a string or a path object. + :param path: + The path to change into. """ if self._cwd is None: self._cwd = os.getcwd() @@ -353,11 +381,14 @@ class MonkeyPatch: There is generally no need to call `undo()`, since it is called automatically during tear-down. - Note that the same `monkeypatch` fixture is used across a - single test function invocation. If `monkeypatch` is used both by - the test function itself and one of the test fixtures, - calling `undo()` will undo all of the changes made in - both functions. + .. note:: + The same `monkeypatch` fixture is used across a + single test function invocation. If `monkeypatch` is used both by + the test function itself and one of the test fixtures, + calling `undo()` will undo all of the changes made in + both functions. + + Prefer to use :meth:`context() <pytest.MonkeyPatch.context>` instead. """ for obj, name, value in reversed(self._setattr): if value is not notset: @@ -368,11 +399,13 @@ class MonkeyPatch: for dictionary, key, value in reversed(self._setitem): if value is notset: try: - del dictionary[key] + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dictionary[key] # type: ignore[attr-defined] except KeyError: pass # Was already deleted, so we have the desired state. else: - dictionary[key] = value + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dictionary[key] = value # type: ignore[index] self._setitem[:] = [] if self._savesyspath is not None: sys.path[:] = self._savesyspath diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nodes.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nodes.py index e49c1b003e0..974d756a2be 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nodes.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nodes.py @@ -1,6 +1,9 @@ -import os -import warnings +# mypy: allow-untyped-defs +import abc +from functools import cached_property from inspect import signature +import os +import pathlib from pathlib import Path from typing import Any from typing import Callable @@ -9,6 +12,7 @@ from typing import Iterable from typing import Iterator from typing import List from typing import MutableMapping +from typing import NoReturn from typing import Optional from typing import overload from typing import Set @@ -17,16 +21,19 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import warnings + +import pluggy import _pytest._code from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest.compat import cached_property +from _pytest._code.code import Traceback from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure -from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH +from _pytest.config.compat import _check_path from _pytest.deprecated import NODE_CTOR_FSPATH_ARG from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator @@ -37,10 +44,13 @@ from _pytest.pathlib import commonpath from _pytest.stash import Stash from _pytest.warning_types import PytestWarning + if TYPE_CHECKING: + from typing import Self + # Imported here due to circular import. - from _pytest.main import Session from _pytest._code.code import _TracebackStyle + from _pytest.main import Session SEP = "/" @@ -48,57 +58,7 @@ SEP = "/" tracebackcutdir = Path(_pytest.__file__).parent -def iterparentnodeids(nodeid: str) -> Iterator[str]: - """Return the parent node IDs of a given node ID, inclusive. - - For the node ID - - "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" - - the result would be - - "" - "testing" - "testing/code" - "testing/code/test_excinfo.py" - "testing/code/test_excinfo.py::TestFormattedExcinfo" - "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" - - Note that / components are only considered until the first ::. - """ - pos = 0 - first_colons: Optional[int] = nodeid.find("::") - if first_colons == -1: - first_colons = None - # The root Session node - always present. - yield "" - # Eagerly consume SEP parts until first colons. - while True: - at = nodeid.find(SEP, pos, first_colons) - if at == -1: - break - if at > 0: - yield nodeid[:at] - pos = at + len(SEP) - # Eagerly consume :: parts. - while True: - at = nodeid.find("::", pos) - if at == -1: - break - if at > 0: - yield nodeid[:at] - pos = at + len("::") - # The node ID itself. - if nodeid: - yield nodeid - - -def _check_path(path: Path, fspath: LEGACY_PATH) -> None: - if Path(fspath) != path: - raise ValueError( - f"Path({fspath!r}) != {path!r}\n" - "if both path and fspath are given they need to be equal" - ) +_T = TypeVar("_T") def _imply_path( @@ -111,7 +71,7 @@ def _imply_path( NODE_CTOR_FSPATH_ARG.format( node_type_name=node_type.__name__, ), - stacklevel=3, + stacklevel=6, ) if path is not None: if fspath is not None: @@ -125,48 +85,63 @@ def _imply_path( _NodeType = TypeVar("_NodeType", bound="Node") -class NodeMeta(type): - def __call__(self, *k, **kw): +class NodeMeta(abc.ABCMeta): + """Metaclass used by :class:`Node` to enforce that direct construction raises + :class:`Failed`. + + This behaviour supports the indirection introduced with :meth:`Node.from_parent`, + the named constructor to be used instead of direct construction. The design + decision to enforce indirection with :class:`NodeMeta` was made as a + temporary aid for refactoring the collection tree, which was diagnosed to + have :class:`Node` objects whose creational patterns were overly entangled. + Once the refactoring is complete, this metaclass can be removed. + + See https://github.com/pytest-dev/pytest/projects/3 for an overview of the + progress on detangling the :class:`Node` classes. + """ + + def __call__(cls, *k, **kw) -> NoReturn: msg = ( "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" "See " "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent" " for more details." - ).format(name=f"{self.__module__}.{self.__name__}") + ).format(name=f"{cls.__module__}.{cls.__name__}") fail(msg, pytrace=False) - def _create(self, *k, **kw): + def _create(cls: Type[_T], *k, **kw) -> _T: try: - return super().__call__(*k, **kw) + return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] except TypeError: - sig = signature(getattr(self, "__init__")) + sig = signature(getattr(cls, "__init__")) known_kw = {k: v for k, v in kw.items() if k in sig.parameters} from .warning_types import PytestDeprecationWarning warnings.warn( PytestDeprecationWarning( - f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n" + f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" "See https://docs.pytest.org/en/stable/deprecations.html" "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " "for more details." ) ) - return super().__call__(*k, **known_kw) + return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc] -class Node(metaclass=NodeMeta): - """Base class for Collector and Item, the components of the test - collection tree. +class Node(abc.ABC, metaclass=NodeMeta): + r"""Base class of :class:`Collector` and :class:`Item`, the components of + the test collection tree. - Collector subclasses have children; Items are leaf nodes. + ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the + leaf nodes. """ # Implemented in the legacypath plugin. #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage #: for methods not migrated to ``pathlib.Path`` yet, such as - #: :meth:`Item.reportinfo`. Will be deprecated in a future release, prefer - #: using :attr:`path` instead. + #: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in + #: a future release, prefer using :attr:`path` instead. fspath: LEGACY_PATH # Use __slots__ to make attribute access faster. @@ -193,7 +168,7 @@ class Node(metaclass=NodeMeta): nodeid: Optional[str] = None, ) -> None: #: A unique name within the scope of the parent node. - self.name = name + self.name: str = name #: The parent collector node. self.parent = parent @@ -208,7 +183,7 @@ class Node(metaclass=NodeMeta): if session: #: The pytest session this node is part of. - self.session = session + self.session: Session = session else: if not parent: raise TypeError("session or parent must be provided") @@ -217,7 +192,7 @@ class Node(metaclass=NodeMeta): if path is None and fspath is None: path = getattr(parent, "path", None) #: Filesystem path where this node was collected from (can be None). - self.path: Path = _imply_path(type(self), path, fspath=fspath) + self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath) # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. @@ -239,14 +214,12 @@ class Node(metaclass=NodeMeta): #: A place where plugins can store information on the node for their #: own use. - #: - #: :type: Stash - self.stash = Stash() + self.stash: Stash = Stash() # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash @classmethod - def from_parent(cls, parent: "Node", **kw): + def from_parent(cls, parent: "Node", **kw) -> "Self": """Public constructor for Nodes. This indirection got introduced in order to enable removing @@ -264,7 +237,7 @@ class Node(metaclass=NodeMeta): return cls._create(parent=parent, **kw) @property - def ihook(self): + def ihook(self) -> pluggy.HookRelay: """fspath-sensitive hook proxy used to call pytest hooks.""" return self.session.gethookproxy(self.path) @@ -295,9 +268,7 @@ class Node(metaclass=NodeMeta): # enforce type checks here to avoid getting a generic type error later otherwise. if not isinstance(warning, Warning): raise ValueError( - "warning must be an instance of Warning or subclass, got {!r}".format( - warning - ) + f"warning must be an instance of Warning or subclass, got {warning!r}" ) path, lineno = get_fslocation_from_item(self) assert lineno is not None @@ -324,9 +295,20 @@ class Node(metaclass=NodeMeta): def teardown(self) -> None: pass + def iter_parents(self) -> Iterator["Node"]: + """Iterate over all parent collectors starting from and including self + up to the root of the collection tree. + + .. versionadded:: 8.1 + """ + parent: Optional[Node] = self + while parent is not None: + yield parent + parent = parent.parent + def listchain(self) -> List["Node"]: - """Return list of all parent collectors up to self, starting from - the root of collection tree.""" + """Return a list of all parent collectors starting from the root of the + collection tree down to and including self.""" chain = [] item: Optional[Node] = self while item is not None: @@ -340,6 +322,8 @@ class Node(metaclass=NodeMeta): ) -> None: """Dynamically add a marker object to the node. + :param marker: + The marker. :param append: Whether to append the marker, or prepend it. """ @@ -361,6 +345,7 @@ class Node(metaclass=NodeMeta): """Iterate over all markers of the node. :param name: If given, filter the results by the name attribute. + :returns: An iterator of the markers of the node. """ return (x[1] for x in self.iter_markers_with_node(name=name)) @@ -372,18 +357,16 @@ class Node(metaclass=NodeMeta): :param name: If given, filter the results by the name attribute. :returns: An iterator of (node, mark) tuples. """ - for node in reversed(self.listchain()): + for node in self.iter_parents(): for mark in node.own_markers: if name is None or getattr(mark, "name", None) == name: yield node, mark @overload - def get_closest_marker(self, name: str) -> Optional[Mark]: - ... + def get_closest_marker(self, name: str) -> Optional[Mark]: ... @overload - def get_closest_marker(self, name: str, default: Mark) -> Mark: - ... + def get_closest_marker(self, name: str, default: Mark) -> Mark: ... def get_closest_marker( self, name: str, default: Optional[Mark] = None @@ -407,7 +390,8 @@ class Node(metaclass=NodeMeta): return [x.name for x in self.listchain()] def addfinalizer(self, fin: Callable[[], object]) -> None: - """Register a function to be called when this node is finalized. + """Register a function to be called without arguments when this node is + finalized. This method can only be called when this node is active in a setup chain, for example during self.setup(). @@ -415,16 +399,19 @@ class Node(metaclass=NodeMeta): self.session._setupstate.addfinalizer(fin, self) def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]: - """Get the next parent node (including self) which is an instance of - the given class.""" - current: Optional[Node] = self - while current and not isinstance(current, cls): - current = current.parent - assert current is None or isinstance(current, cls) - return current - - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - pass + """Get the closest parent node (including self) which is an instance of + the given class. + + :param cls: The node class to search for. + :returns: The node, if found. + """ + for node in self.iter_parents(): + if isinstance(node, cls): + return node + return None + + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + return excinfo.traceback def _repr_failure_py( self, @@ -434,19 +421,19 @@ class Node(metaclass=NodeMeta): from _pytest.fixtures import FixtureLookupError if isinstance(excinfo.value, ConftestImportFailure): - excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo) + excinfo = ExceptionInfo.from_exception(excinfo.value.cause) if isinstance(excinfo.value, fail.Exception): if not excinfo.value.pytrace: style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() + + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] if self.config.getoption("fulltrace", False): style = "long" + tbfilter = False else: - tb = _pytest._code.Traceback([excinfo.traceback[-1]]) - self._prunetraceback(excinfo) - if len(excinfo.traceback) == 0: - excinfo.traceback = tb + tbfilter = self._traceback_filter if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -477,7 +464,7 @@ class Node(metaclass=NodeMeta): abspath=abspath, showlocals=self.config.getoption("showlocals", False), style=style, - tbfilter=False, # pruned already, or in --fulltrace mode. + tbfilter=tbfilter, truncate_locals=truncate_locals, ) @@ -500,9 +487,9 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i * "location": a pair (path, lineno) * "obj": a Python object that the node wraps. - * "fspath": just a path + * "path": just a path - :rtype: A tuple of (str|Path, int) with filename and line number. + :rtype: A tuple of (str|Path, int) with filename and 0-based line number. """ # See Item.location. location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) @@ -511,19 +498,22 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i obj = getattr(node, "obj", None) if obj is not None: return getfslineno(obj) - return getattr(node, "fspath", "unknown location"), -1 + return getattr(node, "path", "unknown location"), -1 + +class Collector(Node, abc.ABC): + """Base class of all collectors. -class Collector(Node): - """Collector instances create children through collect() and thus - iteratively build a tree.""" + Collector create children through `collect()` and thus iteratively build + the collection tree. + """ class CollectError(Exception): """An error during collection, contains a custom message.""" + @abc.abstractmethod def collect(self) -> Iterable[Union["Item", "Collector"]]: - """Return a list of children (items and collectors) for this - collection node.""" + """Collect children (items and collectors) for this collector.""" raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. @@ -548,13 +538,14 @@ class Collector(Node): return self._repr_failure_py(excinfo, style=tbstyle) - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: if hasattr(self, "path"): traceback = excinfo.traceback ntraceback = traceback.cut(path=self.path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter() + return ntraceback.filter(excinfo) + return excinfo.traceback def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]: @@ -565,7 +556,9 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[ return None -class FSCollector(Collector): +class FSCollector(Collector, abc.ABC): + """Base class for filesystem collectors.""" + def __init__( self, fspath: Optional[LEGACY_PATH] = None, @@ -628,28 +621,38 @@ class FSCollector(Collector): fspath: Optional[LEGACY_PATH] = None, path: Optional[Path] = None, **kw, - ): + ) -> "Self": """The public constructor.""" return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) - def gethookproxy(self, fspath: "os.PathLike[str]"): - warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) - return self.session.gethookproxy(fspath) - - def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: - warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) - return self.session.isinitpath(path) - -class File(FSCollector): +class File(FSCollector, abc.ABC): """Base class for collecting tests from a file. :ref:`non-python tests`. """ -class Item(Node): - """A basic test invocation item. +class Directory(FSCollector, abc.ABC): + """Base class for collecting files from a directory. + + A basic directory collector does the following: goes over the files and + sub-directories in the directory and creates collectors for them by calling + the hooks :hook:`pytest_collect_directory` and :hook:`pytest_collect_file`, + after checking that they are not ignored using + :hook:`pytest_ignore_collect`. + + The default directory collectors are :class:`~pytest.Dir` and + :class:`~pytest.Package`. + + .. versionadded:: 8.0 + + :ref:`custom directory collectors`. + """ + + +class Item(Node, abc.ABC): + """Base class of all test invocation items. Note that for a single function there might be multiple test invocation items. """ @@ -714,6 +717,7 @@ class Item(Node): PytestWarning, ) + @abc.abstractmethod def runtest(self) -> None: """Run the test case for this item. @@ -746,7 +750,7 @@ class Item(Node): Returns a tuple with three elements: - The path of the test (default ``self.path``) - - The line number of the test (default ``None``) + - The 0-based line number of the test (default ``None``) - A name of the test to be shown (default ``""``) .. seealso:: :ref:`non-python tests` @@ -755,8 +759,13 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: + """ + Returns a tuple of ``(relfspath, lineno, testname)`` for this item + where ``relfspath`` is file path relative to ``config.rootpath`` + and lineno is a 0-based line number. + """ location = self.reportinfo() - path = absolutepath(os.fspath(location[0])) + path = absolutepath(location[0]) relfspath = self.session._node_location_to_relpath(path) assert type(location[2]) is str return (relfspath, location[1], location[2]) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nose.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nose.py deleted file mode 100644 index b0699d22bd8..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/nose.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Run testsuites written for nose.""" -from _pytest.config import hookimpl -from _pytest.fixtures import getfixturemarker -from _pytest.nodes import Item -from _pytest.python import Function -from _pytest.unittest import TestCaseFunction - - -@hookimpl(trylast=True) -def pytest_runtest_setup(item: Item) -> None: - if not isinstance(item, Function): - return - # Don't do nose style setup/teardown on direct unittest style classes. - if isinstance(item, TestCaseFunction): - return - - # Capture the narrowed type of item for the teardown closure, - # see https://github.com/python/mypy/issues/2608 - func = item - - call_optional(func.obj, "setup") - func.addfinalizer(lambda: call_optional(func.obj, "teardown")) - - # NOTE: Module- and class-level fixtures are handled in python.py - # with `pluginmanager.has_plugin("nose")` checks. - # It would have been nicer to implement them outside of core, but - # it's not straightforward. - - -def call_optional(obj: object, name: str) -> bool: - method = getattr(obj, name, None) - if method is None: - return False - is_fixture = getfixturemarker(method) is not None - if is_fixture: - return False - if not callable(method): - return False - # If there are any problems allow the exception to raise rather than - # silently ignoring it. - method() - return True diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/outcomes.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/outcomes.py index 25206fe0e85..f953dabe03d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/outcomes.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/outcomes.py @@ -1,28 +1,17 @@ """Exception classes and constants handling test outcomes as well as functions creating them.""" + import sys -import warnings from typing import Any from typing import Callable from typing import cast +from typing import NoReturn from typing import Optional +from typing import Protocol from typing import Type from typing import TypeVar -from _pytest.deprecated import KEYWORD_MSG_ARG - -TYPE_CHECKING = False # Avoid circular import through compat. - -if TYPE_CHECKING: - from typing import NoReturn - from typing_extensions import Protocol -else: - # typing.Protocol is only available starting from Python 3.8. It is also - # available from typing_extensions, but we don't want a runtime dependency - # on that. So use a dummy runtime implementation. - from typing import Generic - - Protocol = Generic +from .warning_types import PytestDeprecationWarning class OutcomeException(BaseException): @@ -114,8 +103,9 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E @_with_exception(Exit) def exit( - reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None -) -> "NoReturn": + reason: str = "", + returncode: Optional[int] = None, +) -> NoReturn: """Exit testing process. :param reason: @@ -123,30 +113,21 @@ def exit( only because `msg` is deprecated. :param returncode: - Return code to be used when exiting pytest. + Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`. - :param msg: - Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. + :raises pytest.exit.Exception: + The exception that is raised. """ __tracebackhide__ = True - from _pytest.config import UsageError - - if reason and msg: - raise UsageError( - "cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`." - ) - if not reason: - if msg is None: - raise UsageError("exit() requires a reason argument") - warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2) - reason = msg raise Exit(reason, returncode) @_with_exception(Skipped) def skip( - reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None -) -> "NoReturn": + reason: str = "", + *, + allow_module_level: bool = False, +) -> NoReturn: """Skip an executing test with the given message. This function should be called only during testing (setup, call or teardown) or @@ -157,11 +138,15 @@ def skip( The message to show the user as reason for the skip. :param allow_module_level: - Allows this function to be called at module level, skipping the rest - of the module. Defaults to False. + Allows this function to be called at module level. + Raising the skip exception at module level will stop + the execution of the module and prevent the collection of all tests in the module, + even those defined before the `skip` call. + + Defaults to False. - :param msg: - Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. + :raises pytest.skip.Exception: + The exception that is raised. .. note:: It is better to use the :ref:`pytest.mark.skipif ref` marker when @@ -171,14 +156,11 @@ def skip( to skip a doctest statically. """ __tracebackhide__ = True - reason = _resolve_msg_to_reason("skip", reason, msg) raise Skipped(msg=reason, allow_module_level=allow_module_level) @_with_exception(Failed) -def fail( - reason: str = "", pytrace: bool = True, msg: Optional[str] = None -) -> "NoReturn": +def fail(reason: str = "", pytrace: bool = True) -> NoReturn: """Explicitly fail an executing test with the given message. :param reason: @@ -188,108 +170,137 @@ def fail( If False, msg represents the full failure information and no python traceback will be reported. - :param msg: - Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. + :raises pytest.fail.Exception: + The exception that is raised. """ __tracebackhide__ = True - reason = _resolve_msg_to_reason("fail", reason, msg) raise Failed(msg=reason, pytrace=pytrace) -def _resolve_msg_to_reason( - func_name: str, reason: str, msg: Optional[str] = None -) -> str: - """ - Handles converting the deprecated msg parameter if provided into - reason, raising a deprecation warning. This function will be removed - when the optional msg argument is removed from here in future. - - :param str func_name: - The name of the offending function, this is formatted into the deprecation message. - - :param str reason: - The reason= passed into either pytest.fail() or pytest.skip() - - :param str msg: - The msg= passed into either pytest.fail() or pytest.skip(). This will - be converted into reason if it is provided to allow pytest.skip(msg=) or - pytest.fail(msg=) to continue working in the interim period. - - :returns: - The value to use as reason. - - """ - __tracebackhide__ = True - if msg is not None: - - if reason: - from pytest import UsageError - - raise UsageError( - f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted." - ) - warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3) - reason = msg - return reason - - class XFailed(Failed): """Raised from an explicit call to pytest.xfail().""" @_with_exception(XFailed) -def xfail(reason: str = "") -> "NoReturn": +def xfail(reason: str = "") -> NoReturn: """Imperatively xfail an executing test or setup function with the given reason. This function should be called only during testing (setup, call or teardown). + No other code is executed after using ``xfail()`` (it is implemented + internally by raising an exception). + + :param reason: + The message to show the user as reason for the xfail. + .. note:: It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be xfailed under certain conditions like known bugs or missing features. + + :raises pytest.xfail.Exception: + The exception that is raised. """ __tracebackhide__ = True raise XFailed(reason) def importorskip( - modname: str, minversion: Optional[str] = None, reason: Optional[str] = None + modname: str, + minversion: Optional[str] = None, + reason: Optional[str] = None, + *, + exc_type: Optional[Type[ImportError]] = None, ) -> Any: """Import and return the requested module ``modname``, or skip the current test if the module cannot be imported. - :param str modname: + :param modname: The name of the module to import. - :param str minversion: + :param minversion: If given, the imported module's ``__version__`` attribute must be at least this minimal version, otherwise the test is still skipped. - :param str reason: + :param reason: If given, this reason is shown as the message when the module cannot be imported. + :param exc_type: + The exception that should be captured in order to skip modules. + Must be :py:class:`ImportError` or a subclass. + + If the module can be imported but raises :class:`ImportError`, pytest will + issue a warning to the user, as often users expect the module not to be + found (which would raise :class:`ModuleNotFoundError` instead). + + This warning can be suppressed by passing ``exc_type=ImportError`` explicitly. + + See :ref:`import-or-skip-import-error` for details. + :returns: The imported module. This should be assigned to its canonical name. + :raises pytest.skip.Exception: + If the module cannot be imported. + Example:: docutils = pytest.importorskip("docutils") + + .. versionadded:: 8.2 + + The ``exc_type`` parameter. """ import warnings __tracebackhide__ = True compile(modname, "", "eval") # to catch syntaxerrors + # Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError), + # as this might be hiding an installation/environment problem, which is not usually what is intended + # when using importorskip() (#11523). + # In 9.1, to keep the function signature compatible, we just change the code below to: + # 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given. + # 2. Remove `warn_on_import` and the warning handling. + if exc_type is None: + exc_type = ImportError + warn_on_import_error = True + else: + warn_on_import_error = False + + skipped: Optional[Skipped] = None + warning: Optional[Warning] = None + with warnings.catch_warnings(): # Make sure to ignore ImportWarnings that might happen because # of existing directories with the same name we're trying to # import but without a __init__.py file. warnings.simplefilter("ignore") + try: __import__(modname) - except ImportError as exc: + except exc_type as exc: + # Do not raise or issue warnings inside the catch_warnings() block. if reason is None: reason = f"could not import {modname!r}: {exc}" - raise Skipped(reason, allow_module_level=True) from None + skipped = Skipped(reason, allow_module_level=True) + + if warn_on_import_error and not isinstance(exc, ModuleNotFoundError): + lines = [ + "", + f"Module '{modname}' was found, but when imported by pytest it raised:", + f" {exc!r}", + "In pytest 9.1 this warning will become an error by default.", + "You can fix the underlying problem, or alternatively overwrite this behavior and silence this " + "warning by passing exc_type=ImportError explicitly.", + "See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror", + ] + warning = PytestDeprecationWarning("\n".join(lines)) + + if warning: + warnings.warn(warning, stacklevel=2) + if skipped: + raise skipped + mod = sys.modules[modname] if minversion is None: return mod @@ -300,8 +311,7 @@ def importorskip( if verattr is None or Version(verattr) < Version(minversion): raise Skipped( - "module %r has __version__ %r, required is: %r" - % (modname, verattr, minversion), + f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}", allow_module_level=True, ) return mod diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pastebin.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pastebin.py index 385b3022cc0..533d78c9a2a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pastebin.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pastebin.py @@ -1,15 +1,17 @@ +# mypy: allow-untyped-defs """Submit failure or test session information to a pastebin service.""" -import tempfile + from io import StringIO +import tempfile from typing import IO from typing import Union -import pytest from _pytest.config import Config from _pytest.config import create_terminal_writer from _pytest.config.argparsing import Parser from _pytest.stash import StashKey from _pytest.terminal import TerminalReporter +import pytest pastebinfile_key = StashKey[IO[bytes]]() @@ -24,7 +26,7 @@ def pytest_addoption(parser: Parser) -> None: dest="pastebin", default=None, choices=["failed", "all"], - help="send failed|all info to bpaste.net pastebin service.", + help="Send failed|all info to bpaste.net pastebin service", ) @@ -73,8 +75,8 @@ def create_new_paste(contents: Union[str, bytes]) -> str: :returns: URL to the pasted contents, or an error message. """ import re - from urllib.request import urlopen from urllib.parse import urlencode + from urllib.request import urlopen params = {"code": contents, "lexer": "text", "expiry": "1week"} url = "https://bpa.st" diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pathlib.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pathlib.py index b44753e1a41..b11eea4e7ef 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pathlib.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pathlib.py @@ -1,19 +1,16 @@ import atexit import contextlib -import fnmatch -import importlib.util -import itertools -import os -import shutil -import sys -import uuid -import warnings from enum import Enum from errno import EBADF from errno import ELOOP from errno import ENOENT from errno import ENOTDIR +import fnmatch from functools import partial +from importlib.machinery import ModuleSpec +import importlib.util +import itertools +import os from os.path import expanduser from os.path import expandvars from os.path import isabs @@ -21,20 +18,30 @@ from os.path import sep from pathlib import Path from pathlib import PurePath from posixpath import sep as posix_sep +import shutil +import sys +import types from types import ModuleType +from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import Iterator +from typing import List from typing import Optional from typing import Set +from typing import Tuple +from typing import Type from typing import TypeVar from typing import Union +import uuid +import warnings from _pytest.compat import assert_never from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning + LOCK_TIMEOUT = 60 * 60 * 24 * 3 @@ -52,7 +59,7 @@ _IGNORED_WINERRORS = ( ) -def _ignore_error(exception): +def _ignore_error(exception: Exception) -> bool: return ( getattr(exception, "errno", None) in _IGNORED_ERRORS or getattr(exception, "winerror", None) in _IGNORED_WINERRORS @@ -63,21 +70,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: return path.joinpath(".lock") -def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: +def on_rm_rf_error( + func: Optional[Callable[..., Any]], + path: str, + excinfo: Union[ + BaseException, + Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]], + ], + *, + start_path: Path, +) -> bool: """Handle known read-only errors during rmtree. The returned value is used only by our own tests. """ - exctype, excvalue = exc[:2] + if isinstance(excinfo, BaseException): + exc = excinfo + else: + exc = excinfo[1] # Another process removed the file in the middle of the "rm_rf" (xdist for example). # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 - if isinstance(excvalue, FileNotFoundError): + if isinstance(exc, FileNotFoundError): return False - if not isinstance(excvalue, PermissionError): + if not isinstance(exc, PermissionError): warnings.warn( - PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}") + PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}") ) return False @@ -85,9 +104,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: if func not in (os.open,): warnings.warn( PytestWarning( - "(rm_rf) unknown function {} when removing {}:\n{}: {}".format( - func, path, exctype, excvalue - ) + f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}" ) ) return False @@ -149,26 +166,29 @@ def rm_rf(path: Path) -> None: are read-only.""" path = ensure_extended_length_path(path) onerror = partial(on_rm_rf_error, start_path=path) - shutil.rmtree(str(path), onerror=onerror) + if sys.version_info >= (3, 12): + shutil.rmtree(str(path), onexc=onerror) + else: + shutil.rmtree(str(path), onerror=onerror) -def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: - """Find all elements in root that begin with the prefix, case insensitive.""" +def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]: + """Find all elements in root that begin with the prefix, case-insensitive.""" l_prefix = prefix.lower() - for x in root.iterdir(): + for x in os.scandir(root): if x.name.lower().startswith(l_prefix): yield x -def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]: +def extract_suffixes(iter: Iterable["os.DirEntry[str]"], prefix: str) -> Iterator[str]: """Return the parts of the paths following the prefix. :param iter: Iterator over path names. :param prefix: Expected prefix of the path names. """ p_len = len(prefix) - for p in iter: - yield p.name[p_len:] + for entry in iter: + yield entry.name[p_len:] def find_suffixes(root: Path, prefix: str) -> Iterator[str]: @@ -176,7 +196,7 @@ def find_suffixes(root: Path, prefix: str) -> Iterator[str]: return extract_suffixes(find_prefixed(root, prefix), prefix) -def parse_num(maybe_num) -> int: +def parse_num(maybe_num: str) -> int: """Parse number path suffixes, returns -1 on error.""" try: return int(maybe_num) @@ -223,7 +243,7 @@ def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: else: raise OSError( "could not create numbered dir with prefix " - "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) + f"{prefix} in {root} after 10 tries" ) @@ -244,7 +264,9 @@ def create_cleanup_lock(p: Path) -> Path: return lock_path -def register_cleanup_lock_removal(lock_path: Path, register=atexit.register): +def register_cleanup_lock_removal( + lock_path: Path, register: Any = atexit.register +) -> Any: """Register a cleanup function for removing a lock, by default on atexit.""" pid = os.getpid() @@ -327,23 +349,34 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: """List candidates for numbered directories to be removed - follows py.path.""" max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) max_delete = max_existing - keep - paths = find_prefixed(root, prefix) - paths, paths2 = itertools.tee(paths) - numbers = map(parse_num, extract_suffixes(paths2, prefix)) - for path, number in zip(paths, numbers): + entries = find_prefixed(root, prefix) + entries, entries2 = itertools.tee(entries) + numbers = map(parse_num, extract_suffixes(entries2, prefix)) + for entry, number in zip(entries, numbers): if number <= max_delete: - yield path + yield Path(entry) + + +def cleanup_dead_symlinks(root: Path) -> None: + for left_dir in root.iterdir(): + if left_dir.is_symlink(): + if not left_dir.resolve().exists(): + left_dir.unlink() def cleanup_numbered_dir( root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float ) -> None: """Cleanup for lock driven numbered directories.""" + if not root.exists(): + return for path in cleanup_candidates(root, prefix, keep): try_cleanup(path, consider_lock_dead_if_created_before) for path in root.glob("garbage-*"): try_cleanup(path, consider_lock_dead_if_created_before) + cleanup_dead_symlinks(root) + def make_numbered_dir_with_cleanup( root: Path, @@ -357,8 +390,10 @@ def make_numbered_dir_with_cleanup( for i in range(10): try: p = make_numbered_dir(root, prefix, mode) - lock_path = create_cleanup_lock(p) - register_cleanup_lock_removal(lock_path) + # Only lock the current dir when keep is not 0 + if keep != 0: + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path) except Exception as exc: e = exc else: @@ -426,10 +461,14 @@ def parts(s: str) -> Set[str]: return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} -def symlink_or_skip(src, dst, **kwargs): +def symlink_or_skip( + src: Union["os.PathLike[str]", str], + dst: Union["os.PathLike[str]", str], + **kwargs: Any, +) -> None: """Make a symlink, or skip the test in case symlinks are not supported.""" try: - os.symlink(str(src), str(dst), **kwargs) + os.symlink(src, dst, **kwargs) except OSError as e: skip(f"symlinks not supported: {e}") @@ -452,71 +491,90 @@ class ImportPathMismatchError(ImportError): def import_path( - p: Union[str, "os.PathLike[str]"], + path: Union[str, "os.PathLike[str]"], *, mode: Union[str, ImportMode] = ImportMode.prepend, root: Path, + consider_namespace_packages: bool, ) -> ModuleType: - """Import and return a module from the given path, which can be a file (a module) or + """ + Import and return a module from the given path, which can be a file (a module) or a directory (a package). - The import mechanism used is controlled by the `mode` parameter: + :param path: + Path to the file to import. - * `mode == ImportMode.prepend`: the directory containing the module (or package, taking - `__init__.py` files into account) will be put at the *start* of `sys.path` before - being imported with `__import__. + :param mode: + Controls the underlying import mechanism that will be used: - * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended - to the end of `sys.path`, if not already in `sys.path`. + * ImportMode.prepend: the directory containing the module (or package, taking + `__init__.py` files into account) will be put at the *start* of `sys.path` before + being imported with `importlib.import_module`. - * `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib` - to import the module, which avoids having to use `__import__` and muck with `sys.path` - at all. It effectively allows having same-named test modules in different places. + * ImportMode.append: same as `prepend`, but the directory will be appended + to the end of `sys.path`, if not already in `sys.path`. + + * ImportMode.importlib: uses more fine control mechanisms provided by `importlib` + to import the module, which avoids having to muck with `sys.path` at all. It effectively + allows having same-named test modules in different places. :param root: Used as an anchor when mode == ImportMode.importlib to obtain a unique name for the module being imported so it can safely be stored into ``sys.modules``. + :param consider_namespace_packages: + If True, consider namespace packages when resolving module names. + :raises ImportPathMismatchError: If after importing the given `path` and the module `__file__` are different. Only raised in `prepend` and `append` modes. """ + path = Path(path) mode = ImportMode(mode) - path = Path(p) - if not path.exists(): raise ImportError(path) if mode is ImportMode.importlib: - module_name = module_name_from_path(path, root) - - for meta_importer in sys.meta_path: - spec = meta_importer.find_spec(module_name, [str(path.parent)]) - if spec is not None: - break + # Try to import this module using the standard import mechanisms, but + # without touching sys.path. + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pass else: - spec = importlib.util.spec_from_file_location(module_name, str(path)) + # If the given module name is already in sys.modules, do not import it again. + with contextlib.suppress(KeyError): + return sys.modules[module_name] + + mod = _import_module_using_spec( + module_name, path, pkg_root, insert_modules=False + ) + if mod is not None: + return mod + + # Could not import the module with the current sys.path, so we fall back + # to importing the file as a single module, not being a part of a package. + module_name = module_name_from_path(path, root) + with contextlib.suppress(KeyError): + return sys.modules[module_name] - if spec is None: + mod = _import_module_using_spec( + module_name, path, path.parent, insert_modules=True + ) + if mod is None: raise ImportError(f"Can't find module {module_name} at location {path}") - mod = importlib.util.module_from_spec(spec) - sys.modules[module_name] = mod - spec.loader.exec_module(mod) # type: ignore[union-attr] - insert_missing_modules(sys.modules, module_name) return mod - pkg_path = resolve_package_path(path) - if pkg_path is not None: - pkg_root = pkg_path.parent - names = list(path.with_suffix("").relative_to(pkg_root).parts) - if names[-1] == "__init__": - names.pop() - module_name = ".".join(names) - else: - pkg_root = path.parent - module_name = path.stem + try: + pkg_root, module_name = resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) + except CouldNotResolvePathError: + pkg_root, module_name = path.parent, path.stem # Change sys.path permanently: restoring it at the end of this function would cause surprising # problems because of delayed imports: for example, a conftest.py file imported by this function @@ -539,10 +597,13 @@ def import_path( ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "") if ignore != "1": module_file = mod.__file__ + if module_file is None: + raise ImportPathMismatchError(module_name, module_file, path) + if module_file.endswith((".pyc", ".pyo")): module_file = module_file[:-1] - if module_file.endswith(os.path.sep + "__init__.py"): - module_file = module_file[: -(len(os.path.sep + "__init__.py"))] + if module_file.endswith(os.sep + "__init__.py"): + module_file = module_file[: -(len(os.sep + "__init__.py"))] try: is_same = _is_same(str(path), module_file) @@ -555,6 +616,80 @@ def import_path( return mod +def _import_module_using_spec( + module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool +) -> Optional[ModuleType]: + """ + Tries to import a module by its canonical name, path to the .py file, and its + parent location. + + :param insert_modules: + If True, will call insert_missing_modules to create empty intermediate modules + for made-up module names (when importing test files not reachable from sys.path). + """ + # Checking with sys.meta_path first in case one of its hooks can import this module, + # such as our own assertion-rewrite hook. + for meta_importer in sys.meta_path: + spec = meta_importer.find_spec(module_name, [str(module_location)]) + if spec_matches_module_path(spec, module_path): + break + else: + spec = importlib.util.spec_from_file_location(module_name, str(module_path)) + + if spec_matches_module_path(spec, module_path): + assert spec is not None + # Attempt to import the parent module, seems is our responsibility: + # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 + parent_module_name, _, name = module_name.rpartition(".") + parent_module: Optional[ModuleType] = None + if parent_module_name: + parent_module = sys.modules.get(parent_module_name) + if parent_module is None: + # Find the directory of this module's parent. + parent_dir = ( + module_path.parent.parent + if module_path.name == "__init__.py" + else module_path.parent + ) + # Consider the parent module path as its __init__.py file, if it has one. + parent_module_path = ( + parent_dir / "__init__.py" + if (parent_dir / "__init__.py").is_file() + else parent_dir + ) + parent_module = _import_module_using_spec( + parent_module_name, + parent_module_path, + parent_dir, + insert_modules=insert_modules, + ) + + # Find spec and import this module. + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) # type: ignore[union-attr] + + # Set this module as an attribute of the parent module (#12194). + if parent_module is not None: + setattr(parent_module, name, mod) + + if insert_modules: + insert_missing_modules(sys.modules, module_name) + return mod + + return None + + +def spec_matches_module_path( + module_spec: Optional[ModuleSpec], module_path: Path +) -> bool: + """Return true if the given ModuleSpec can be used to import the given module path.""" + if module_spec is None or module_spec.origin is None: + return False + + return Path(module_spec.origin) == module_path + + # Implement a special _is_same function on Windows which returns True if the two filenames # compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678). if sys.platform.startswith("win"): @@ -562,7 +697,6 @@ if sys.platform.startswith("win"): def _is_same(f1: str, f2: str) -> bool: return Path(f1) == Path(f2) or os.path.samefile(f1, f2) - else: def _is_same(f1: str, f2: str) -> bool: @@ -587,6 +721,16 @@ def module_name_from_path(path: Path, root: Path) -> str: # Use the parts for the relative path to the root path. path_parts = relative_path.parts + # Module name for packages do not contain the __init__ file, unless + # the `__init__.py` file is at the root. + if len(path_parts) >= 2 and path_parts[-1] == "__init__": + path_parts = path_parts[:-1] + + # Module names cannot contain ".", normalize them to "_". This prevents + # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. + # Also, important to replace "." at the start of paths, as those are considered relative imports. + path_parts = tuple(x.replace(".", "_") for x in path_parts) + return ".".join(path_parts) @@ -600,12 +744,30 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> """ module_parts = module_name.split(".") while module_name: - if module_name not in modules: - module = ModuleType( - module_name, - doc="Empty module created by pytest's importmode=importlib.", - ) - modules[module_name] = module + parent_module_name, _, child_name = module_name.rpartition(".") + if parent_module_name: + parent_module = modules.get(parent_module_name) + if parent_module is None: + try: + # If sys.meta_path is empty, calling import_module will issue + # a warning and raise ModuleNotFoundError. To avoid the + # warning, we check sys.meta_path explicitly and raise the error + # ourselves to fall back to creating a dummy module. + if not sys.meta_path: + raise ModuleNotFoundError + parent_module = importlib.import_module(parent_module_name) + except ModuleNotFoundError: + parent_module = ModuleType( + module_name, + doc="Empty module created by pytest's importmode=importlib.", + ) + modules[parent_module_name] = parent_module + + # Add child attribute to the parent that can reference the child + # modules. + if not hasattr(parent_module, child_name): + setattr(parent_module, child_name, modules[module_name]) + module_parts.pop(-1) module_name = ".".join(module_parts) @@ -614,12 +776,12 @@ def resolve_package_path(path: Path) -> Optional[Path]: """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. - Returns None if it can not be determined. + Returns None if it cannot be determined. """ result = None for parent in itertools.chain((path,), path.parents): if parent.is_dir(): - if not parent.joinpath("__init__.py").is_file(): + if not (parent / "__init__.py").is_file(): break if not parent.name.isidentifier(): break @@ -627,42 +789,148 @@ def resolve_package_path(path: Path) -> Optional[Path]: return result +def resolve_pkg_root_and_module_name( + path: Path, *, consider_namespace_packages: bool = False +) -> Tuple[Path, str]: + """ + Return the path to the directory of the root package that contains the + given Python file, and its module name: + + src/ + app/ + __init__.py + core/ + __init__.py + models.py + + Passing the full path to `models.py` will yield Path("src") and "app.core.models". + + If consider_namespace_packages is True, then we additionally check upwards in the hierarchy + for namespace packages: + + https://packaging.python.org/en/latest/guides/packaging-namespace-packages + + Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). + """ + pkg_root: Optional[Path] = None + pkg_path = resolve_package_path(path) + if pkg_path is not None: + pkg_root = pkg_path.parent + if consider_namespace_packages: + start = pkg_root if pkg_root is not None else path.parent + for candidate in (start, *start.parents): + module_name = compute_module_name(candidate, path) + if module_name and is_importable(module_name, path): + # Point the pkg_root to the root of the namespace package. + pkg_root = candidate + break + + if pkg_root is not None: + module_name = compute_module_name(pkg_root, path) + if module_name: + return pkg_root, module_name + + raise CouldNotResolvePathError(f"Could not resolve for {path}") + + +def is_importable(module_name: str, module_path: Path) -> bool: + """ + Return if the given module path could be imported normally by Python, akin to the user + entering the REPL and importing the corresponding module name directly, and corresponds + to the module_path specified. + + :param module_name: + Full module name that we want to check if is importable. + For example, "app.models". + + :param module_path: + Full path to the python module/package we want to check if is importable. + For example, "/projects/src/app/models.py". + """ + try: + # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through + # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``). + # Using importlib.util.find_spec() is different, it gives the same results as trying to import + # the module normally in the REPL. + spec = importlib.util.find_spec(module_name) + except (ImportError, ValueError, ImportWarning): + return False + else: + return spec_matches_module_path(spec, module_path) + + +def compute_module_name(root: Path, module_path: Path) -> Optional[str]: + """Compute a module name based on a path and a root anchor.""" + try: + path_without_suffix = module_path.with_suffix("") + except ValueError: + # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter). + return None + + try: + relative = path_without_suffix.relative_to(root) + except ValueError: # pragma: no cover + return None + names = list(relative.parts) + if not names: + return None + if names[-1] == "__init__": + names.pop() + return ".".join(names) + + +class CouldNotResolvePathError(Exception): + """Custom exception raised by resolve_pkg_root_and_module_name.""" + + +def scandir( + path: Union[str, "os.PathLike[str]"], + sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name, +) -> List["os.DirEntry[str]"]: + """Scan a directory recursively, in breadth-first order. + + The returned entries are sorted according to the given key. + The default is to sort by name. + """ + entries = [] + with os.scandir(path) as s: + # Skip entries with symlink loops and other brokenness, so the caller + # doesn't have to deal with it. + for entry in s: + try: + entry.is_file() + except OSError as err: + if _ignore_error(err): + continue + raise + entries.append(entry) + entries.sort(key=sort_key) # type: ignore[arg-type] + return entries + + def visit( path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool] ) -> Iterator["os.DirEntry[str]"]: """Walk a directory recursively, in breadth-first order. + The `recurse` predicate determines whether a directory is recursed. + Entries at each directory level are sorted. """ - - # Skip entries with symlink loops and other brokenness, so the caller doesn't - # have to deal with it. - entries = [] - for entry in os.scandir(path): - try: - entry.is_file() - except OSError as err: - if _ignore_error(err): - continue - raise - entries.append(entry) - - entries.sort(key=lambda entry: entry.name) - + entries = scandir(path) yield from entries - for entry in entries: if entry.is_dir() and recurse(entry): yield from visit(entry.path, recurse) -def absolutepath(path: Union[Path, str]) -> Path: +def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path: """Convert a path to an absolute path using os.path.abspath. Prefer this over Path.resolve() (see #6523). Prefer this over Path.absolute() (not public, doesn't normalize). """ - return Path(os.path.abspath(str(path))) + return Path(os.path.abspath(path)) def commonpath(path1: Path, path2: Path) -> Optional[Path]: @@ -706,19 +974,11 @@ def bestrelpath(directory: Path, dest: Path) -> str: ) -# Originates from py. path.local.copy(), with siginficant trims and adjustments. -# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True) -def copytree(source: Path, target: Path) -> None: - """Recursively copy a source directory to target.""" - assert source.is_dir() - for entry in visit(source, recurse=lambda entry: not entry.is_symlink()): - x = Path(entry) - relpath = x.relative_to(source) - newx = target / relpath - newx.parent.mkdir(exist_ok=True) - if x.is_symlink(): - newx.symlink_to(os.readlink(x)) - elif x.is_file(): - shutil.copyfile(x, newx) - elif x.is_dir(): - newx.mkdir(exist_ok=True) +def safe_exists(p: Path) -> bool: + """Like Path.exists(), but account for input arguments that might be too long (#11394).""" + try: + return p.exists() + except (ValueError, OSError): + # ValueError: stat: path too long for Windows + # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect + return False diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester.py index 363a3727447..9ba8e6a8182 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester.py @@ -1,28 +1,34 @@ +# mypy: allow-untyped-defs """(Disabled by default) support for testing pytest and pytest plugins. PYTEST_DONT_REWRITE """ + import collections.abc import contextlib +from fnmatch import fnmatch import gc import importlib +from io import StringIO +import locale import os +from pathlib import Path import platform import re import shutil import subprocess import sys import traceback -from fnmatch import fnmatch -from io import StringIO -from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import Final +from typing import final from typing import Generator from typing import IO from typing import Iterable from typing import List +from typing import Literal from typing import Optional from typing import overload from typing import Sequence @@ -39,7 +45,6 @@ from iniconfig import SectionWrapper from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture -from _pytest.compat import final from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin @@ -60,7 +65,6 @@ from _pytest.outcomes import fail from _pytest.outcomes import importorskip from _pytest.outcomes import skip from _pytest.pathlib import bestrelpath -from _pytest.pathlib import copytree from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -69,9 +73,6 @@ from _pytest.warning_types import PytestWarning if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - import pexpect @@ -89,7 +90,7 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", dest="lsof", default=False, - help="run FD checks if lsof is available", + help="Run FD checks if lsof is available", ) parser.addoption( @@ -98,13 +99,13 @@ def pytest_addoption(parser: Parser) -> None: dest="runpytest", choices=("inprocess", "subprocess"), help=( - "run pytest sub runs in tests using an 'inprocess' " + "Run pytest sub runs in tests using an 'inprocess' " "or 'subprocess' (python -m main) method" ), ) parser.addini( - "pytester_example_dir", help="directory to take the pytester example files from" + "pytester_example_dir", help="Directory to take the pytester example files from" ) @@ -123,12 +124,18 @@ def pytest_configure(config: Config) -> None: class LsofFdLeakChecker: def get_open_files(self) -> List[Tuple[str, str]]: + if sys.version_info >= (3, 11): + # New in Python 3.11, ignores utf-8 mode + encoding = locale.getencoding() + else: + encoding = locale.getpreferredencoding(False) out = subprocess.run( ("lsof", "-Ffn0", "-p", str(os.getpid())), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True, - universal_newlines=True, + text=True, + encoding=encoding, ).stdout def isopen(line: str) -> bool: @@ -161,29 +168,31 @@ class LsofFdLeakChecker: else: return True - @hookimpl(hookwrapper=True, tryfirst=True) - def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: + @hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]: lines1 = self.get_open_files() - yield - if hasattr(sys, "pypy_version_info"): - gc.collect() - lines2 = self.get_open_files() - - new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: - error = [ - "***** %s FD leakage detected" % len(leaked_files), - *(str(f) for f in leaked_files), - "*** Before:", - *(str(f) for f in lines1), - "*** After:", - *(str(f) for f in lines2), - "***** %s FD leakage detected" % len(leaked_files), - "*** function %s:%s: %s " % item.location, - "See issue #2366", - ] - item.warn(PytestWarning("\n".join(error))) + try: + return (yield) + finally: + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [ + "***** %s FD leakage detected" % len(leaked_files), + *(str(f) for f in leaked_files), + "*** Before:", + *(str(f) for f in lines1), + "*** After:", + *(str(f) for f in lines2), + "***** %s FD leakage detected" % len(leaked_files), + "*** function {}:{}: {} ".format(*item.location), + "See issue #2366", + ] + item.warn(PytestWarning("\n".join(error))) # used at least by pytest-xdist plugin @@ -237,8 +246,7 @@ class RecordedHookCall: if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. - def __getattr__(self, key: str): - ... + def __getattr__(self, key: str): ... @final @@ -281,7 +289,8 @@ class HookRecorder: __tracebackhide__ = True i = 0 entries = list(entries) - backlocals = sys._getframe(1).f_locals + # Since Python 3.13, f_locals is not a dict, but eval requires a dict. + backlocals = dict(sys._getframe(1).f_locals) while entries: name, check = entries.pop(0) for ind, call in enumerate(self.calls[i:]): @@ -319,15 +328,13 @@ class HookRecorder: def getreports( self, names: "Literal['pytest_collectreport']", - ) -> Sequence[CollectReport]: - ... + ) -> Sequence[CollectReport]: ... @overload def getreports( self, names: "Literal['pytest_runtest_logreport']", - ) -> Sequence[TestReport]: - ... + ) -> Sequence[TestReport]: ... @overload def getreports( @@ -336,8 +343,7 @@ class HookRecorder: "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: - ... + ) -> Sequence[Union[CollectReport, TestReport]]: ... def getreports( self, @@ -369,14 +375,12 @@ class HookRecorder: values.append(rep) if not values: raise ValueError( - "could not find test report matching %r: " - "no test reports at all!" % (inamepart,) + f"could not find test report matching {inamepart!r}: " + "no test reports at all!" ) if len(values) > 1: raise ValueError( - "found 2 or more testreports matching {!r}: {}".format( - inamepart, values - ) + f"found 2 or more testreports matching {inamepart!r}: {values}" ) return values[0] @@ -384,15 +388,13 @@ class HookRecorder: def getfailures( self, names: "Literal['pytest_collectreport']", - ) -> Sequence[CollectReport]: - ... + ) -> Sequence[CollectReport]: ... @overload def getfailures( self, names: "Literal['pytest_runtest_logreport']", - ) -> Sequence[TestReport]: - ... + ) -> Sequence[TestReport]: ... @overload def getfailures( @@ -401,8 +403,7 @@ class HookRecorder: "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: - ... + ) -> Sequence[Union[CollectReport, TestReport]]: ... def getfailures( self, @@ -477,7 +478,9 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: @fixture -def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": +def pytester( + request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch +) -> "Pytester": """ Facilities to write tests/configuration files, execute pytest in isolation, and match against expected output, perfect for black-box testing of pytest plugins. @@ -488,7 +491,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` fixture but provides methods which aid in testing pytest itself. """ - return Pytester(request, tmp_path_factory, _ispytest=True) + return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True) @fixture @@ -622,14 +625,6 @@ class RunResult: ) -class CwdSnapshot: - def __init__(self) -> None: - self.__saved = os.getcwd() - - def restore(self) -> None: - os.chdir(self.__saved) - - class SysModulesSnapshot: def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None: self.__preserve = preserve @@ -659,17 +654,7 @@ class Pytester: against expected output, perfect for black-box testing of pytest plugins. It attempts to isolate the test run from external factors as much as possible, modifying - the current working directory to ``path`` and environment variables during initialization. - - Attributes: - - :ivar Path path: temporary directory path used to create files/run tests from, etc. - - :ivar plugins: - A list of plugins to use with :py:meth:`parseconfig` and - :py:meth:`runpytest`. Initially this is an empty list but plugins can - be added to the list. The type of items to add to the list depends on - the method using them so refer to them for details. + the current working directory to :attr:`path` and environment variables during initialization. """ __test__ = False @@ -683,6 +668,7 @@ class Pytester: self, request: FixtureRequest, tmp_path_factory: TempPathFactory, + monkeypatch: MonkeyPatch, *, _ispytest: bool = False, ) -> None: @@ -697,16 +683,19 @@ class Pytester: name = request.node.name self._name = name self._path: Path = tmp_path_factory.mktemp(name, numbered=True) + #: A list of plugins to use with :py:meth:`parseconfig` and + #: :py:meth:`runpytest`. Initially this is an empty list but plugins can + #: be added to the list. The type of items to add to the list depends on + #: the method using them so refer to them for details. self.plugins: List[Union[str, _PluggyPlugin]] = [] - self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() - self.chdir() self._request.addfinalizer(self._finalize) self._method = self._request.config.getoption("--runpytest") self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) - self._monkeypatch = mp = MonkeyPatch() + self._monkeypatch = mp = monkeypatch + self.chdir() mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) # Ensure no unexpected caching via tox. mp.delenv("TOX_ENV_DIR", raising=False) @@ -721,7 +710,7 @@ class Pytester: @property def path(self) -> Path: - """Temporary directory where files are created and pytest is executed.""" + """Temporary directory path used to create files/run tests from, etc.""" return self._path def __repr__(self) -> str: @@ -737,8 +726,6 @@ class Pytester: """ self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() - self._cwd_snapshot.restore() - self._monkeypatch.undo() def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # Some zope modules used by twisted-related tests keep internal state @@ -753,8 +740,8 @@ class Pytester: return SysModulesSnapshot(preserve=preserve_module) def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: - """Create a new :py:class:`HookRecorder` for a PluginManager.""" - pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) + """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`.""" + pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined] self._request.addfinalizer(reprec.finish_recording) return reprec @@ -763,7 +750,7 @@ class Pytester: This is done automatically upon instantiation. """ - os.chdir(self.path) + self._monkeypatch.chdir(self.path) def _makefile( self, @@ -774,6 +761,9 @@ class Pytester: ) -> Path: items = list(files.items()) + if ext is None: + raise TypeError("ext must not be None") + if ext and not ext.startswith("."): raise ValueError( f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" @@ -802,7 +792,7 @@ class Pytester: def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: r"""Create new text file(s) in the test directory. - :param str ext: + :param ext: The extension the file(s) should use, including the dot, e.g. `.py`. :param args: All args are treated as strings and joined using newlines. @@ -811,9 +801,10 @@ class Pytester: :param kwargs: Each keyword is the name of a file, while the value of it will be written as contents of the file. + :returns: + The first created file. Examples: - .. code-block:: python pytester.makefile(".txt", "line1", "line2") @@ -830,11 +821,19 @@ class Pytester: return self._makefile(ext, args, kwargs) def makeconftest(self, source: str) -> Path: - """Write a contest.py file with 'source' as contents.""" + """Write a conftest.py file. + + :param source: The contents. + :returns: The conftest.py file. + """ return self.makepyfile(conftest=source) def makeini(self, source: str) -> Path: - """Write a tox.ini file with 'source' as contents.""" + """Write a tox.ini file. + + :param source: The contents. + :returns: The tox.ini file. + """ return self.makefile(".ini", tox=source) def getinicfg(self, source: str) -> SectionWrapper: @@ -843,7 +842,10 @@ class Pytester: return IniConfig(str(p))["pytest"] def makepyprojecttoml(self, source: str) -> Path: - """Write a pyproject.toml file with 'source' as contents. + """Write a pyproject.toml file. + + :param source: The contents. + :returns: The pyproject.ini file. .. versionadded:: 6.0 """ @@ -856,7 +858,6 @@ class Pytester: existing files. Examples: - .. code-block:: python def test_something(pytester): @@ -876,7 +877,6 @@ class Pytester: existing files. Examples: - .. code-block:: python def test_something(pytester): @@ -896,19 +896,28 @@ class Pytester: This is undone automatically when this object dies at the end of each test. + + :param path: + The path. """ if path is None: path = self.path self._monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name: str) -> Path: - """Create a new (sub)directory.""" + def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path: + """Create a new (sub)directory. + + :param name: + The name of the directory, relative to the pytester path. + :returns: + The created directory. + """ p = self.path / name p.mkdir() return p - def mkpydir(self, name: str) -> Path: + def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path: """Create a new python package. This creates a (sub)directory with an empty ``__init__.py`` file so it @@ -922,14 +931,15 @@ class Pytester: def copy_example(self, name: Optional[str] = None) -> Path: """Copy file from project's directory into the testdir. - :param str name: The name of the file to copy. - :return: path to the copied directory (inside ``self.path``). - + :param name: + The name of the file to copy. + :return: + Path to the copied directory (inside ``self.path``). """ - example_dir = self._request.config.getini("pytester_example_dir") - if example_dir is None: + example_dir_ = self._request.config.getini("pytester_example_dir") + if example_dir_ is None: raise ValueError("pytester_example_dir is unset, can't copy examples") - example_dir = self._request.config.rootpath / example_dir + example_dir: Path = self._request.config.rootpath / example_dir_ for extra_element in self._request.node.iter_markers("pytester_example_path"): assert extra_element.args @@ -952,7 +962,7 @@ class Pytester: example_path = example_dir.joinpath(name) if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): - copytree(example_path, self.path) + shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True) return self.path elif example_path.is_file(): result = self.path.joinpath(example_path.name) @@ -965,14 +975,16 @@ class Pytester: def getnode( self, config: Config, arg: Union[str, "os.PathLike[str]"] - ) -> Optional[Union[Collector, Item]]: - """Return the collection node of a file. + ) -> Union[Collector, Item]: + """Get the collection node of a file. - :param pytest.Config config: + :param config: A pytest config. See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. - :param os.PathLike[str] arg: + :param arg: Path to the file. + :returns: + The node. """ session = Session.from_config(config) assert "::" not in str(arg) @@ -982,13 +994,18 @@ class Pytester: config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode(self, path: Union[str, "os.PathLike[str]"]): + def getpathnode( + self, path: Union[str, "os.PathLike[str]"] + ) -> Union[Collector, Item]: """Return the collection node of a file. This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to create the (configured) pytest Config instance. - :param os.PathLike[str] path: Path to the file. + :param path: + Path to the file. + :returns: + The node. """ path = Path(path) config = self.parseconfigure(path) @@ -1004,6 +1021,11 @@ class Pytester: This recurses into the collection node and returns a list of all the test items contained within. + + :param colitems: + The collection nodes. + :returns: + The collected items. """ session = colitems[0].session result: List[Item] = [] @@ -1017,7 +1039,7 @@ class Pytester: The calling test instance (class containing the test method) must provide a ``.getrunner()`` method which should return a runner which can run the test protocol for a single item, e.g. - :py:func:`_pytest.runner.runtestprotocol`. + ``_pytest.runner.runtestprotocol``. """ # used from runner functional tests item = self.getitem(source) @@ -1037,11 +1059,11 @@ class Pytester: :param cmdlineargs: Any extra command line arguments to use. """ p = self.makepyfile(source) - values = list(cmdlineargs) + [p] + values = [*list(cmdlineargs), p] return self.inline_run(*values) def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: - """Run ``pytest.main(['--collectonly'])`` in-process. + """Run ``pytest.main(['--collect-only'])`` in-process. Runs the :py:func:`pytest.main` function to run all of pytest inside the test process itself like :py:meth:`inline_run`, but returns a @@ -1190,15 +1212,16 @@ class Pytester: return new_args def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: - """Return a new pytest Config instance from given commandline args. + """Return a new pytest :class:`pytest.Config` instance from given + commandline args. - This invokes the pytest bootstrapping code in _pytest.config to create - a new :py:class:`_pytest.core.PluginManager` and call the - pytest_cmdline_parse hook to create a new - :py:class:`pytest.Config` instance. + This invokes the pytest bootstrapping code in _pytest.config to create a + new :py:class:`pytest.PytestPluginManager` and call the + :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config` + instance. - If :py:attr:`plugins` has been populated they should be plugin modules - to be registered with the PluginManager. + If :attr:`plugins` has been populated they should be plugin modules + to be registered with the plugin manager. """ import _pytest.config @@ -1216,7 +1239,8 @@ class Pytester: """Return a new pytest configured Config instance. Returns a new :py:class:`pytest.Config` instance like - :py:meth:`parseconfig`, but also calls the pytest_configure hook. + :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure` + hook. """ config = self.parseconfig(*args) config._do_configure() @@ -1235,14 +1259,14 @@ class Pytester: The module source. :param funcname: The name of the test function for which to return a test item. + :returns: + The test item. """ items = self.getitems(source) for item in items: if item.name == funcname: return item - assert 0, "{!r} item not found in module:\n{}\nitems: {}".format( - funcname, source, items - ) + assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}" def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]: """Return all test items collected from the module. @@ -1364,7 +1388,7 @@ class Pytester: :param stdin: Optional standard input. - - If it is :py:attr:`CLOSE_STDIN` (Default), then this method calls + - If it is ``CLOSE_STDIN`` (Default), then this method calls :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and the standard input is closed immediately after the new command is started. @@ -1375,6 +1399,8 @@ class Pytester: - Otherwise, it is passed through to :py:class:`subprocess.Popen`. For further information in this case, consult the document of the ``stdin`` parameter in :py:class:`subprocess.Popen`. + :returns: + The result. """ __tracebackhide__ = True @@ -1399,10 +1425,7 @@ class Pytester: def handle_timeout() -> None: __tracebackhide__ = True - timeout_message = ( - "{seconds} second timeout expired running:" - " {command}".format(seconds=timeout, command=cmdargs) - ) + timeout_message = f"{timeout} second timeout expired running: {cmdargs}" popen.kill() popen.wait() @@ -1461,13 +1484,15 @@ class Pytester: :param timeout: The period in seconds after which to timeout and raise :py:class:`Pytester.TimeoutExpired`. + :returns: + The result. """ __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) - args = ("--basetemp=%s" % p,) + args + args = ("--basetemp=%s" % p, *args) plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: - args = ("-p", plugins[0]) + args + args = ("-p", plugins[0], *args) args = self._getpytestargs() + args return self.run(*args, timeout=timeout) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py index 657e4db5fc3..d20c2bb5999 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/pytester_assertions.py @@ -1,4 +1,5 @@ """Helper plugin for pytester; should not be loaded on its own.""" + # This plugin contains assertions used by pytester. pytester cannot # contain them itself, since it is imported by the `pytest` module, # hence cannot be subject to assertion rewriting, which requires a diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python.py index 0fd5702a5cc..41a2fe39af3 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python.py @@ -1,23 +1,27 @@ +# mypy: allow-untyped-defs """Python test discovery, setup and run of test functions.""" + +import abc +from collections import Counter +from collections import defaultdict +import dataclasses import enum import fnmatch +from functools import partial import inspect import itertools import os -import sys -import types -import warnings -from collections import Counter -from collections import defaultdict -from functools import partial from pathlib import Path +import types from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import Pattern @@ -26,8 +30,7 @@ from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import Union - -import attr +import warnings import _pytest from _pytest import fixtures @@ -36,30 +39,26 @@ from _pytest._code import filter_traceback from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest._io import TerminalWriter +from _pytest._code.code import Traceback from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped -from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func from _pytest.compat import getimfunc -from _pytest.compat import getlocation from _pytest.compat import is_async_function from _pytest.compat import is_generator from _pytest.compat import LEGACY_PATH from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass -from _pytest.compat import STRING_TYPES from _pytest.config import Config -from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest -from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH -from _pytest.deprecated import INSTANCE_COLLECTOR +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FuncFixtureInfo +from _pytest.fixtures import get_scope_node from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet @@ -69,80 +68,51 @@ from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip -from _pytest.pathlib import bestrelpath from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError -from _pytest.pathlib import parts -from _pytest.pathlib import visit +from _pytest.pathlib import scandir +from _pytest.scope import _ScopeName from _pytest.scope import Scope +from _pytest.stash import StashKey from _pytest.warning_types import PytestCollectionWarning +from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning -if TYPE_CHECKING: - from typing_extensions import Literal - from _pytest.scope import _ScopeName - -_PYTEST_DIR = Path(_pytest.__file__).parent +if TYPE_CHECKING: + from typing import Self def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group.addoption( - "--fixtures", - "--funcargs", - action="store_true", - dest="showfixtures", - default=False, - help="show available fixtures, sorted by plugin appearance " - "(fixtures with leading '_' are only shown with '-v')", - ) - group.addoption( - "--fixtures-per-test", - action="store_true", - dest="show_fixtures_per_test", - default=False, - help="show fixtures per test", - ) parser.addini( "python_files", type="args", # NOTE: default is also used in AssertionRewritingHook. default=["test_*.py", "*_test.py"], - help="glob-style file patterns for Python test module discovery", + help="Glob-style file patterns for Python test module discovery", ) parser.addini( "python_classes", type="args", default=["Test"], - help="prefixes or glob names for Python test class discovery", + help="Prefixes or glob names for Python test class discovery", ) parser.addini( "python_functions", type="args", default=["test"], - help="prefixes or glob names for Python test function and method discovery", + help="Prefixes or glob names for Python test function and method discovery", ) parser.addini( "disable_test_id_escaping_and_forfeit_all_rights_to_community_support", type="bool", default=False, - help="disable string escape non-ascii characters, might cause unwanted " + help="Disable string escape non-ASCII characters, might cause unwanted " "side effects(use at your own risk)", ) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.showfixtures: - showfixtures(config) - return 0 - if config.option.show_fixtures_per_test: - show_fixtures_per_test(config) - return 0 - return None - - def pytest_generate_tests(metafunc: "Metafunc") -> None: for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) @@ -192,14 +162,35 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: result = testfunction(**testargs) if hasattr(result, "__await__") or hasattr(result, "__aiter__"): async_warn_and_skip(pyfuncitem.nodeid) + elif result is not None: + warnings.warn( + PytestReturnNotNoneWarning( + f"Expected None, but {pyfuncitem.nodeid} returned {result!r}, which will be an error in a " + "future version of pytest. Did you mean to use `assert` instead of `return`?" + ) + ) return True +def pytest_collect_directory( + path: Path, parent: nodes.Collector +) -> Optional[nodes.Collector]: + pkginit = path / "__init__.py" + try: + has_pkginit = pkginit.is_file() + except PermissionError: + # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096. + return None + if has_pkginit: + return Package.from_parent(parent, path=path) + return None + + def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]: if file_path.suffix == ".py": if not parent.session.isinitpath(file_path): if not path_matches_patterns( - file_path, parent.config.getini("python_files") + ["__init__.py"] + file_path, parent.config.getini("python_files") ): return None ihook = parent.session.gethookproxy(file_path) @@ -216,15 +207,14 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module": - if module_path.name == "__init__.py": - pkg: Package = Package.from_parent(parent, path=module_path) - return pkg - mod: Module = Module.from_parent(parent, path=module_path) - return mod + return Module.from_parent(parent, path=module_path) @hookimpl(trylast=True) -def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): +def pytest_pycollect_makeitem( + collector: Union["Module", "Class"], name: str, obj: object +) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]: + assert isinstance(collector, (Class, Module)), type(collector) # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): if collector.istestclass(obj, name): @@ -248,14 +238,15 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): elif getattr(obj, "__test__", True): if is_generator(obj): res = Function.from_parent(collector, name=name) - reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( - name=name + reason = ( + f"yield tests were removed in pytest 4.0 - {name} will be ignored" ) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestCollectionWarning(reason)) + return res else: - res = list(collector._genfunctions(name, obj)) - return res + return list(collector._genfunctions(name, obj)) + return None class PyobjMixin(nodes.Node): @@ -283,10 +274,10 @@ class PyobjMixin(nodes.Node): """Python instance object the function is bound to. Returns None if not a test method, e.g. for a standalone test function, - a staticmethod, a class or a module. + a class or a module. """ - node = self.getparent(Function) - return getattr(node.obj, "__self__", None) if node is not None else None + # Overridden by Function. + return None @property def obj(self): @@ -298,6 +289,9 @@ class PyobjMixin(nodes.Node): # used to avoid Function marker duplication if self._ALLOW_MARKERS: self.own_markers.extend(get_unpacked_marks(self.obj)) + # This assumes that `obj` is called before there is a chance + # to add custom keys to `self.keywords`, so no fear of overriding. + self.keywords.update((mark.name, mark) for mark in self.own_markers) return obj @obj.setter @@ -313,10 +307,8 @@ class PyobjMixin(nodes.Node): def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: """Return Python path relative to the containing module.""" - chain = self.listchain() - chain.reverse() parts = [] - for node in chain: + for node in self.iter_parents(): name = node.name if isinstance(node, Module): name = os.path.splitext(name)[0] @@ -330,19 +322,8 @@ class PyobjMixin(nodes.Node): def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: # XXX caching? - obj = self.obj - compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) - if isinstance(compat_co_firstlineno, int): - # nose compatibility - file_path = sys.modules[obj.__module__].__file__ - if file_path.endswith(".pyc"): - file_path = file_path[:-1] - path: Union["os.PathLike[str]", str] = file_path - lineno = compat_co_firstlineno - else: - path, lineno = getfslineno(obj) + path, lineno = getfslineno(self.obj) modpath = self.getmodpath() - assert isinstance(lineno, int) return path, lineno, modpath @@ -351,7 +332,7 @@ class PyobjMixin(nodes.Node): # hook is not called for them. # fmt: off class _EmptyClass: pass # noqa: E701 -IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305 +IGNORED_ATTRIBUTES = frozenset.union( frozenset(), # Module. dir(types.ModuleType("empty_module")), @@ -366,7 +347,7 @@ del _EmptyClass # fmt: on -class PyCollector(PyobjMixin, nodes.Collector): +class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): def funcnamefilter(self, name: str) -> bool: return self._matches_prefix_or_glob_option("python_functions", name) @@ -384,8 +365,8 @@ class PyCollector(PyobjMixin, nodes.Collector): def istestfunction(self, obj: object, name: str) -> bool: if self.funcnamefilter(name) or self.isnosetest(obj): - if isinstance(obj, staticmethod): - # staticmethods need to be unwrapped. + if isinstance(obj, (staticmethod, classmethod)): + # staticmethods and classmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) return callable(obj) and fixtures.getfixturemarker(obj) is None else: @@ -419,7 +400,7 @@ class PyCollector(PyobjMixin, nodes.Collector): for basecls in self.obj.__mro__: dicts.append(basecls.__dict__) - # In each class, nodes should be definition ordered. Since Python 3.6, + # In each class, nodes should be definition ordered. # __dict__ is definition ordered. seen: Set[str] = set() dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = [] @@ -482,13 +463,11 @@ class PyCollector(PyobjMixin, nodes.Collector): if not metafunc._calls: yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: - # Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs. - fm = self.session._fixturemanager - fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm) - - # Add_funcarg_pseudo_fixture_def may have shadowed some fixtures - # with direct parametrization, so make sure we update what the - # function really needs. + # Direct parametrizations taking place in module/class-specific + # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure + # we update what the function really needs a.k.a its fixture closure. Note that + # direct parametrizations using `@pytest.mark.parametrize` have already been considered + # into making the closure using `ignore_args` arg to `getfixtureclosure`. fixtureinfo.prune_dependency_tree() for callspec in metafunc._calls: @@ -503,63 +482,110 @@ class PyCollector(PyobjMixin, nodes.Collector): ) +def importtestmodule( + path: Path, + config: Config, +): + # We assume we are only called once per module. + importmode = config.getoption("--import-mode") + try: + mod = import_path( + path, + mode=importmode, + root=config.rootpath, + consider_namespace_packages=config.getini("consider_namespace_packages"), + ) + except SyntaxError as e: + raise nodes.Collector.CollectError( + ExceptionInfo.from_current().getrepr(style="short") + ) from e + except ImportPathMismatchError as e: + raise nodes.Collector.CollectError( + "import file mismatch:\n" + "imported module {!r} has this __file__ attribute:\n" + " {}\n" + "which is not the same as the test file we want to collect:\n" + " {}\n" + "HINT: remove __pycache__ / .pyc files and/or use a " + "unique basename for your test file modules".format(*e.args) + ) from e + except ImportError as e: + exc_info = ExceptionInfo.from_current() + if config.getoption("verbose") < 2: + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short") + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = str(exc_repr) + raise nodes.Collector.CollectError( + f"ImportError while importing test module '{path}'.\n" + "Hint: make sure your test modules/packages have valid Python names.\n" + "Traceback:\n" + f"{formatted_tb}" + ) from e + except skip.Exception as e: + if e.allow_module_level: + raise + raise nodes.Collector.CollectError( + "Using pytest.skip outside of a test will skip the entire module. " + "If that's your intention, pass `allow_module_level=True`. " + "If you want to skip a specific test or an entire class, " + "use the @pytest.mark.skip or @pytest.mark.skipif decorators." + ) from e + config.pluginmanager.consider_module(mod) + return mod + + class Module(nodes.File, PyCollector): - """Collector for test classes and functions.""" + """Collector for test classes and functions in a Python module.""" def _getobj(self): - return self._importtestmodule() + return importtestmodule(self.path, self.config) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - self._inject_setup_module_fixture() - self._inject_setup_function_fixture() + self._register_setup_module_fixture() + self._register_setup_function_fixture() self.session._fixturemanager.parsefactories(self) return super().collect() - def _inject_setup_module_fixture(self) -> None: - """Inject a hidden autouse, module scoped fixture into the collected module object + def _register_setup_module_fixture(self) -> None: + """Register an autouse, module-scoped fixture for the collected module object that invokes setUpModule/tearDownModule if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - has_nose = self.config.pluginmanager.has_plugin("nose") setup_module = _get_first_non_fixture_func( self.obj, ("setUpModule", "setup_module") ) - if setup_module is None and has_nose: - # The name "setup" is too common - only treat as fixture if callable. - setup_module = _get_first_non_fixture_func(self.obj, ("setup",)) - if not callable(setup_module): - setup_module = None teardown_module = _get_first_non_fixture_func( self.obj, ("tearDownModule", "teardown_module") ) - if teardown_module is None and has_nose: - teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",)) - # Same as "setup" above - only treat as fixture if callable. - if not callable(teardown_module): - teardown_module = None if setup_module is None and teardown_module is None: return - @fixtures.fixture( - autouse=True, - scope="module", - # Use a unique name to speed up lookup. - name=f"_xunit_setup_module_fixture_{self.obj.__name__}", - ) def xunit_setup_module_fixture(request) -> Generator[None, None, None]: + module = request.module if setup_module is not None: - _call_with_optional_argument(setup_module, request.module) + _call_with_optional_argument(setup_module, module) yield if teardown_module is not None: - _call_with_optional_argument(teardown_module, request.module) + _call_with_optional_argument(teardown_module, module) - self.obj.__pytest_setup_module = xunit_setup_module_fixture + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_module_fixture_{self.obj.__name__}", + func=xunit_setup_module_fixture, + nodeid=self.nodeid, + scope="module", + autouse=True, + ) - def _inject_setup_function_fixture(self) -> None: - """Inject a hidden autouse, function scoped fixture into the collected module object + def _register_setup_function_fixture(self) -> None: + """Register an autouse, function-scoped fixture for the collected module object that invokes setup_function/teardown_function if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with @@ -572,75 +598,44 @@ class Module(nodes.File, PyCollector): if setup_function is None and teardown_function is None: return - @fixtures.fixture( - autouse=True, - scope="function", - # Use a unique name to speed up lookup. - name=f"_xunit_setup_function_fixture_{self.obj.__name__}", - ) def xunit_setup_function_fixture(request) -> Generator[None, None, None]: if request.instance is not None: # in this case we are bound to an instance, so we need to let # setup_method handle this yield return + function = request.function if setup_function is not None: - _call_with_optional_argument(setup_function, request.function) + _call_with_optional_argument(setup_function, function) yield if teardown_function is not None: - _call_with_optional_argument(teardown_function, request.function) + _call_with_optional_argument(teardown_function, function) - self.obj.__pytest_setup_function = xunit_setup_function_fixture + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_function_fixture_{self.obj.__name__}", + func=xunit_setup_function_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) + + +class Package(nodes.Directory): + """Collector for files and directories in a Python packages -- directories + with an `__init__.py` file. + + .. note:: + + Directories without an `__init__.py` file are instead collected by + :class:`~pytest.Dir` by default. Both are :class:`~pytest.Directory` + collectors. + + .. versionchanged:: 8.0 + + Now inherits from :class:`~pytest.Directory`. + """ - def _importtestmodule(self): - # We assume we are only called once per module. - importmode = self.config.getoption("--import-mode") - try: - mod = import_path(self.path, mode=importmode, root=self.config.rootpath) - except SyntaxError as e: - raise self.CollectError( - ExceptionInfo.from_current().getrepr(style="short") - ) from e - except ImportPathMismatchError as e: - raise self.CollectError( - "import file mismatch:\n" - "imported module %r has this __file__ attribute:\n" - " %s\n" - "which is not the same as the test file we want to collect:\n" - " %s\n" - "HINT: remove __pycache__ / .pyc files and/or use a " - "unique basename for your test file modules" % e.args - ) from e - except ImportError as e: - exc_info = ExceptionInfo.from_current() - if self.config.getoption("verbose") < 2: - exc_info.traceback = exc_info.traceback.filter(filter_traceback) - exc_repr = ( - exc_info.getrepr(style="short") - if exc_info.traceback - else exc_info.exconly() - ) - formatted_tb = str(exc_repr) - raise self.CollectError( - "ImportError while importing test module '{path}'.\n" - "Hint: make sure your test modules/packages have valid Python names.\n" - "Traceback:\n" - "{traceback}".format(path=self.path, traceback=formatted_tb) - ) from e - except skip.Exception as e: - if e.allow_module_level: - raise - raise self.CollectError( - "Using pytest.skip outside of a test will skip the entire module. " - "If that's your intention, pass `allow_module_level=True`. " - "If you want to skip a specific test or an entire class, " - "use the @pytest.mark.skip or @pytest.mark.skipif decorators." - ) from e - self.config.pluginmanager.consider_module(mod) - return mod - - -class Package(Module): def __init__( self, fspath: Optional[LEGACY_PATH], @@ -649,13 +644,12 @@ class Package(Module): config=None, session=None, nodeid=None, - path=Optional[Path], + path: Optional[Path] = None, ) -> None: # NOTE: Could be just the following, but kept as-is for compat. - # nodes.FSCollector.__init__(self, fspath, parent=parent) + # super().__init__(self, fspath, parent=parent) session = parent.session - nodes.FSCollector.__init__( - self, + super().__init__( fspath=fspath, path=path, parent=parent, @@ -663,98 +657,51 @@ class Package(Module): session=session, nodeid=nodeid, ) - self.name = self.path.parent.name def setup(self) -> None: + init_mod = importtestmodule(self.path / "__init__.py", self.config) + # Not using fixtures to call setup_module here because autouse fixtures # from packages are not called automatically (#4085). setup_module = _get_first_non_fixture_func( - self.obj, ("setUpModule", "setup_module") + init_mod, ("setUpModule", "setup_module") ) if setup_module is not None: - _call_with_optional_argument(setup_module, self.obj) + _call_with_optional_argument(setup_module, init_mod) teardown_module = _get_first_non_fixture_func( - self.obj, ("tearDownModule", "teardown_module") + init_mod, ("tearDownModule", "teardown_module") ) if teardown_module is not None: - func = partial(_call_with_optional_argument, teardown_module, self.obj) + func = partial(_call_with_optional_argument, teardown_module, init_mod) self.addfinalizer(func) - def gethookproxy(self, fspath: "os.PathLike[str]"): - warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) - return self.session.gethookproxy(fspath) - - def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool: - warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) - return self.session.isinitpath(path) - - def _recurse(self, direntry: "os.DirEntry[str]") -> bool: - if direntry.name == "__pycache__": - return False - fspath = Path(direntry.path) - ihook = self.session.gethookproxy(fspath.parent) - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return False - norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False - return True - - def _collectfile( - self, fspath: Path, handle_dupes: bool = True - ) -> Sequence[nodes.Collector]: - assert ( - fspath.is_file() - ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() - ) - ihook = self.session.gethookproxy(fspath) - if not self.session.isinitpath(fspath): - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return () - - if handle_dupes: - keepduplicates = self.config.getoption("keepduplicates") - if not keepduplicates: - duplicate_paths = self.config.pluginmanager._duplicatepaths - if fspath in duplicate_paths: - return () - else: - duplicate_paths.add(fspath) - - return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return] - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - this_path = self.path.parent - init_module = this_path / "__init__.py" - if init_module.is_file() and path_matches_patterns( - init_module, self.config.getini("python_files") - ): - yield Module.from_parent(self, path=init_module) - pkg_prefixes: Set[Path] = set() - for direntry in visit(str(this_path), recurse=self._recurse): - path = Path(direntry.path) - - # We will visit our own __init__.py file, in which case we skip it. - if direntry.is_file(): - if direntry.name == "__init__.py" and path.parent == this_path: - continue + # Always collect __init__.py first. + def sort_key(entry: "os.DirEntry[str]") -> object: + return (entry.name != "__init__.py", entry.name) - parts_ = parts(direntry.path) - if any( - str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path - for pkg_prefix in pkg_prefixes - ): - continue - - if direntry.is_file(): - yield from self._collectfile(path) - elif not direntry.is_dir(): - # Broken symlink or invalid/missing file. - continue - elif path.joinpath("__init__.py").is_file(): - pkg_prefixes.add(path) + config = self.config + col: Optional[nodes.Collector] + cols: Sequence[nodes.Collector] + ihook = self.ihook + for direntry in scandir(self.path, sort_key): + if direntry.is_dir(): + path = Path(direntry.path) + if not self.session.isinitpath(path, with_parents=True): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + col = ihook.pytest_collect_directory(path=path, parent=self) + if col is not None: + yield col + + elif direntry.is_file(): + path = Path(direntry.path) + if not self.session.isinitpath(path): + if ihook.pytest_ignore_collect(collection_path=path, config=config): + continue + cols = ihook.pytest_collect_file(file_path=path, parent=self) + yield from cols def _call_with_optional_argument(func, arg) -> None: @@ -771,7 +718,8 @@ def _call_with_optional_argument(func, arg) -> None: def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]: """Return the attribute from the given object to be used as a setup/teardown - xunit-style function, but only if not marked as a fixture to avoid calling it twice.""" + xunit-style function, but only if not marked as a fixture to avoid calling it twice. + """ for name in names: meth: Optional[object] = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: @@ -780,10 +728,10 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o class Class(PyCollector): - """Collector for test methods.""" + """Collector for test methods (and nested classes) in a Python class.""" @classmethod - def from_parent(cls, parent, *, name, obj=None, **kw): + def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override] """The public constructor.""" return super().from_parent(name=name, parent=parent, **kw) @@ -797,9 +745,8 @@ class Class(PyCollector): assert self.parent is not None self.warn( PytestCollectionWarning( - "cannot collect test class %r because it has a " - "__init__ constructor (from: %s)" - % (self.obj.__name__, self.parent.nodeid) + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__init__ constructor (from: {self.parent.nodeid})" ) ) return [] @@ -807,105 +754,83 @@ class Class(PyCollector): assert self.parent is not None self.warn( PytestCollectionWarning( - "cannot collect test class %r because it has a " - "__new__ constructor (from: %s)" - % (self.obj.__name__, self.parent.nodeid) + f"cannot collect test class {self.obj.__name__!r} because it has a " + f"__new__ constructor (from: {self.parent.nodeid})" ) ) return [] - self._inject_setup_class_fixture() - self._inject_setup_method_fixture() + self._register_setup_class_fixture() + self._register_setup_method_fixture() self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) return super().collect() - def _inject_setup_class_fixture(self) -> None: - """Inject a hidden autouse, class scoped fixture into the collected class object + def _register_setup_class_fixture(self) -> None: + """Register an autouse, class scoped fixture into the collected class object that invokes setup_class/teardown_class if either or both are available. Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) - teardown_class = getattr(self.obj, "teardown_class", None) + teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",)) if setup_class is None and teardown_class is None: return - @fixtures.fixture( - autouse=True, - scope="class", - # Use a unique name to speed up lookup. - name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", - ) - def xunit_setup_class_fixture(cls) -> Generator[None, None, None]: + def xunit_setup_class_fixture(request) -> Generator[None, None, None]: + cls = request.cls if setup_class is not None: func = getimfunc(setup_class) - _call_with_optional_argument(func, self.obj) + _call_with_optional_argument(func, cls) yield if teardown_class is not None: func = getimfunc(teardown_class) - _call_with_optional_argument(func, self.obj) + _call_with_optional_argument(func, cls) - self.obj.__pytest_setup_class = xunit_setup_class_fixture + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", + func=xunit_setup_class_fixture, + nodeid=self.nodeid, + scope="class", + autouse=True, + ) - def _inject_setup_method_fixture(self) -> None: - """Inject a hidden autouse, function scoped fixture into the collected class object + def _register_setup_method_fixture(self) -> None: + """Register an autouse, function scoped fixture into the collected class object that invokes setup_method/teardown_method if either or both are available. - Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with + Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - has_nose = self.config.pluginmanager.has_plugin("nose") setup_name = "setup_method" setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) - if setup_method is None and has_nose: - setup_name = "setup" - setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) teardown_name = "teardown_method" - teardown_method = getattr(self.obj, teardown_name, None) - if teardown_method is None and has_nose: - teardown_name = "teardown" - teardown_method = getattr(self.obj, teardown_name, None) + teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,)) if setup_method is None and teardown_method is None: return - @fixtures.fixture( - autouse=True, - scope="function", - # Use a unique name to speed up lookup. - name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", - ) - def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]: + def xunit_setup_method_fixture(request) -> Generator[None, None, None]: + instance = request.instance method = request.function if setup_method is not None: - func = getattr(self, setup_name) + func = getattr(instance, setup_name) _call_with_optional_argument(func, method) yield if teardown_method is not None: - func = getattr(self, teardown_name) + func = getattr(instance, teardown_name) _call_with_optional_argument(func, method) - self.obj.__pytest_setup_method = xunit_setup_method_fixture - - -class InstanceDummy: - """Instance used to be a node type between Class and Function. It has been - removed in pytest 7.0. Some plugins exist which reference `pytest.Instance` - only to ignore it; this dummy class keeps them working. This will be removed - in pytest 8.""" - - pass - - -# Note: module __getattr__ only works on Python>=3.7. Unfortunately -# we can't provide this deprecation warning on Python 3.6. -def __getattr__(name: str) -> object: - if name == "Instance": - warnings.warn(INSTANCE_COLLECTOR, 2) - return InstanceDummy - raise AttributeError(f"module {__name__} has no attribute {name}") + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", + func=xunit_setup_method_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) def hasinit(obj: object) -> bool: @@ -923,7 +848,180 @@ def hasnew(obj: object) -> bool: @final -@attr.s(frozen=True, slots=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) +class IdMaker: + """Make IDs for a parametrization.""" + + __slots__ = ( + "argnames", + "parametersets", + "idfn", + "ids", + "config", + "nodeid", + "func_name", + ) + + # The argnames of the parametrization. + argnames: Sequence[str] + # The ParameterSets of the parametrization. + parametersets: Sequence[ParameterSet] + # Optionally, a user-provided callable to make IDs for parameters in a + # ParameterSet. + idfn: Optional[Callable[[Any], Optional[object]]] + # Optionally, explicit IDs for ParameterSets by index. + ids: Optional[Sequence[Optional[object]]] + # Optionally, the pytest config. + # Used for controlling ASCII escaping, and for calling the + # :hook:`pytest_make_parametrize_id` hook. + config: Optional[Config] + # Optionally, the ID of the node being parametrized. + # Used only for clearer error messages. + nodeid: Optional[str] + # Optionally, the ID of the function being parametrized. + # Used only for clearer error messages. + func_name: Optional[str] + + def make_unique_parameterset_ids(self) -> List[str]: + """Make a unique identifier for each ParameterSet, that may be used to + identify the parametrization in a node ID. + + Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is + - user-provided id, if given + - else an id derived from the value, applicable for certain types + - else <argname><parameterset index> + The counter suffix is appended only in case a string wouldn't be unique + otherwise. + """ + resolved_ids = list(self._resolve_ids()) + # All IDs must be unique! + if len(resolved_ids) != len(set(resolved_ids)): + # Record the number of occurrences of each ID. + id_counts = Counter(resolved_ids) + # Map the ID to its next suffix. + id_suffixes: Dict[str, int] = defaultdict(int) + # Suffix non-unique IDs to make them unique. + for index, id in enumerate(resolved_ids): + if id_counts[id] > 1: + suffix = "" + if id and id[-1].isdigit(): + suffix = "_" + new_id = f"{id}{suffix}{id_suffixes[id]}" + while new_id in set(resolved_ids): + id_suffixes[id] += 1 + new_id = f"{id}{suffix}{id_suffixes[id]}" + resolved_ids[index] = new_id + id_suffixes[id] += 1 + assert len(resolved_ids) == len( + set(resolved_ids) + ), f"Internal error: {resolved_ids=}" + return resolved_ids + + def _resolve_ids(self) -> Iterable[str]: + """Resolve IDs for all ParameterSets (may contain duplicates).""" + for idx, parameterset in enumerate(self.parametersets): + if parameterset.id is not None: + # ID provided directly - pytest.param(..., id="...") + yield parameterset.id + elif self.ids and idx < len(self.ids) and self.ids[idx] is not None: + # ID provided in the IDs list - parametrize(..., ids=[...]). + yield self._idval_from_value_required(self.ids[idx], idx) + else: + # ID not provided - generate it. + yield "-".join( + self._idval(val, argname, idx) + for val, argname in zip(parameterset.values, self.argnames) + ) + + def _idval(self, val: object, argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet.""" + idval = self._idval_from_function(val, argname, idx) + if idval is not None: + return idval + idval = self._idval_from_hook(val, argname) + if idval is not None: + return idval + idval = self._idval_from_value(val) + if idval is not None: + return idval + return self._idval_from_argname(argname, idx) + + def _idval_from_function( + self, val: object, argname: str, idx: int + ) -> Optional[str]: + """Try to make an ID for a parameter in a ParameterSet using the + user-provided id callable, if given.""" + if self.idfn is None: + return None + try: + id = self.idfn(val) + except Exception as e: + prefix = f"{self.nodeid}: " if self.nodeid is not None else "" + msg = "error raised while trying to determine id of parameter '{}' at position {}" + msg = prefix + msg.format(argname, idx) + raise ValueError(msg) from e + if id is None: + return None + return self._idval_from_value(id) + + def _idval_from_hook(self, val: object, argname: str) -> Optional[str]: + """Try to make an ID for a parameter in a ParameterSet by calling the + :hook:`pytest_make_parametrize_id` hook.""" + if self.config: + id: Optional[str] = self.config.hook.pytest_make_parametrize_id( + config=self.config, val=val, argname=argname + ) + return id + return None + + def _idval_from_value(self, val: object) -> Optional[str]: + """Try to make an ID for a parameter in a ParameterSet from its value, + if the value type is supported.""" + if isinstance(val, (str, bytes)): + return _ascii_escaped_by_config(val, self.config) + elif val is None or isinstance(val, (float, int, bool, complex)): + return str(val) + elif isinstance(val, Pattern): + return ascii_escaped(val.pattern) + elif val is NOTSET: + # Fallback to default. Note that NOTSET is an enum.Enum. + pass + elif isinstance(val, enum.Enum): + return str(val) + elif isinstance(getattr(val, "__name__", None), str): + # Name of a class, function, module, etc. + name: str = getattr(val, "__name__") + return name + return None + + def _idval_from_value_required(self, val: object, idx: int) -> str: + """Like _idval_from_value(), but fails if the type is not supported.""" + id = self._idval_from_value(val) + if id is not None: + return id + + # Fail. + if self.func_name is not None: + prefix = f"In {self.func_name}: " + elif self.nodeid is not None: + prefix = f"In {self.nodeid}: " + else: + prefix = "" + msg = ( + f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. " + "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__." + ) + fail(msg, pytrace=False) + + @staticmethod + def _idval_from_argname(argname: str, idx: int) -> str: + """Make an ID for a parameter in a ParameterSet from the argument name + and the index of the ParameterSet.""" + return str(argname) + str(idx) + + +@final +@dataclasses.dataclass(frozen=True) class CallSpec2: """A planned parameterized invocation of a test function. @@ -932,25 +1030,21 @@ class CallSpec2: and stored in item.callspec. """ - # arg name -> arg value which will be passed to the parametrized test - # function (direct parameterization). - funcargs: Dict[str, object] = attr.Factory(dict) - # arg name -> arg value which will be passed to a fixture of the same name - # (indirect parametrization). - params: Dict[str, object] = attr.Factory(dict) + # arg name -> arg value which will be passed to a fixture or pseudo-fixture + # of the same name. (indirect or direct parametrization respectively) + params: Dict[str, object] = dataclasses.field(default_factory=dict) # arg name -> arg index. - indices: Dict[str, int] = attr.Factory(dict) + indices: Dict[str, int] = dataclasses.field(default_factory=dict) # Used for sorting parametrized resources. - _arg2scope: Dict[str, Scope] = attr.Factory(dict) + _arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict) # Parts which will be added to the item's name in `[..]` separated by "-". - _idlist: List[str] = attr.Factory(list) + _idlist: Sequence[str] = dataclasses.field(default_factory=tuple) # Marks which will be applied to the item. - marks: List[Mark] = attr.Factory(list) + marks: List[Mark] = dataclasses.field(default_factory=list) def setmulti( self, *, - valtypes: Mapping[str, "Literal['params', 'funcargs']"], argnames: Iterable[str], valset: Iterable[object], id: str, @@ -958,28 +1052,20 @@ class CallSpec2: scope: Scope, param_index: int, ) -> "CallSpec2": - funcargs = self.funcargs.copy() params = self.params.copy() indices = self.indices.copy() - arg2scope = self._arg2scope.copy() + arg2scope = dict(self._arg2scope) for arg, val in zip(argnames, valset): - if arg in params or arg in funcargs: - raise ValueError(f"duplicate {arg!r}") - valtype_for_arg = valtypes[arg] - if valtype_for_arg == "params": - params[arg] = val - elif valtype_for_arg == "funcargs": - funcargs[arg] = val - else: - assert_never(valtype_for_arg) + if arg in params: + raise ValueError(f"duplicate parametrization of {arg!r}") + params[arg] = val indices[arg] = param_index arg2scope[arg] = scope return CallSpec2( - funcargs=funcargs, params=params, - arg2scope=arg2scope, indices=indices, - idlist=[*self._idlist, id], + _arg2scope=arg2scope, + _idlist=[*self._idlist, id], marks=[*self.marks, *normalize_mark_list(marks)], ) @@ -994,6 +1080,14 @@ class CallSpec2: return "-".join(self._idlist) +def get_direct_param_fixture_func(request: FixtureRequest) -> Any: + return request.param + + +# Used for storing pseudo fixturedefs for direct parametrization. +name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]() + + @final class Metafunc: """Objects passed to the :hook:`pytest_generate_tests` hook. @@ -1040,16 +1134,13 @@ class Metafunc: def parametrize( self, - argnames: Union[str, List[str], Tuple[str, ...]], + argnames: Union[str, Sequence[str]], argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], indirect: Union[bool, Sequence[str]] = False, ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] + Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] ] = None, - scope: "Optional[_ScopeName]" = None, + scope: Optional[_ScopeName] = None, *, _param_mark: Optional[Mark] = None, ) -> None: @@ -1058,8 +1149,9 @@ class Metafunc: during the collection phase. If you need to setup expensive resources see about setting indirect to do it rather than at test setup time. - Can be called multiple times, in which case each call parametrizes all - previous parametrizations, e.g. + Can be called multiple times per test function (but only on different + argument names), in which case each call parametrizes all previous + parametrizations, e.g. :: @@ -1114,7 +1206,7 @@ class Metafunc: It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ - argnames, parameters = ParameterSet._for_parametrize( + argnames, parametersets = ParameterSet._for_parametrize( argnames, argvalues, self.function, @@ -1138,30 +1230,81 @@ class Metafunc: self._validate_if_using_arg_names(argnames, indirect) - arg_values_types = self._resolve_arg_value_types(argnames, indirect) - # Use any already (possibly) generated ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from: generated_ids = _param_mark._param_ids_from._param_ids_generated if generated_ids is not None: ids = generated_ids - ids = self._resolve_arg_ids( - argnames, ids, parameters, nodeid=self.definition.nodeid + ids = self._resolve_parameter_set_ids( + argnames, ids, parametersets, nodeid=self.definition.nodeid ) # Store used (possibly generated) ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from and generated_ids is None: object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) + # Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering + # artificial "pseudo" FixtureDef's so that later at test execution time we can + # rely on a proper FixtureDef to exist for fixture setup. + node = None + # If we have a scope that is higher than function, we need + # to make sure we only ever create an according fixturedef on + # a per-scope basis. We thus store and cache the fixturedef on the + # node related to the scope. + if scope_ is not Scope.Function: + collector = self.definition.parent + assert collector is not None + node = get_scope_node(collector, scope_) + if node is None: + # If used class scope and there is no class, use module-level + # collector (for now). + if scope_ is Scope.Class: + assert isinstance(collector, Module) + node = collector + # If used package scope and there is no package, use session + # (for now). + elif scope_ is Scope.Package: + node = collector.session + else: + assert False, f"Unhandled missing scope: {scope}" + if node is None: + name2pseudofixturedef = None + else: + default: Dict[str, FixtureDef[Any]] = {} + name2pseudofixturedef = node.stash.setdefault( + name2pseudofixturedef_key, default + ) + arg_directness = self._resolve_args_directness(argnames, indirect) + for argname in argnames: + if arg_directness[argname] == "indirect": + continue + if name2pseudofixturedef is not None and argname in name2pseudofixturedef: + fixturedef = name2pseudofixturedef[argname] + else: + fixturedef = FixtureDef( + config=self.config, + baseid="", + argname=argname, + func=get_direct_param_fixture_func, + scope=scope_, + params=None, + ids=None, + _ispytest=True, + ) + if name2pseudofixturedef is not None: + name2pseudofixturedef[argname] = fixturedef + self._arg2fixturedefs[argname] = [fixturedef] + # Create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product # of all calls. newcalls = [] for callspec in self._calls or [CallSpec2()]: - for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): + for param_index, (param_id, param_set) in enumerate( + zip(ids, parametersets) + ): newcallspec = callspec.setmulti( - valtypes=arg_values_types, argnames=argnames, valset=param_set.values, id=param_id, @@ -1172,27 +1315,29 @@ class Metafunc: newcalls.append(newcallspec) self._calls = newcalls - def _resolve_arg_ids( + def _resolve_parameter_set_ids( self, argnames: Sequence[str], ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] + Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] ], - parameters: Sequence[ParameterSet], + parametersets: Sequence[ParameterSet], nodeid: str, ) -> List[str]: - """Resolve the actual ids for the given argnames, based on the ``ids`` parameter given - to ``parametrize``. + """Resolve the actual ids for the given parameter sets. - :param List[str] argnames: List of argument names passed to ``parametrize()``. - :param ids: The ids parameter of the parametrized call (see docs). - :param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``. - :param str str: The nodeid of the item that generated this parametrized call. - :rtype: List[str] - :returns: The list of ids for each argname given. + :param argnames: + Argument names passed to ``parametrize()``. + :param ids: + The `ids` parameter of the ``parametrize()`` call (see docs). + :param parametersets: + The parameter sets, each containing a set of values corresponding + to ``argnames``. + :param nodeid str: + The nodeid of the definition item that generated this + parametrization. + :returns: + List with ids for each parameter set given. """ if ids is None: idfn = None @@ -1202,15 +1347,24 @@ class Metafunc: ids_ = None else: idfn = None - ids_ = self._validate_ids(ids, parameters, self.function.__name__) - return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid) + ids_ = self._validate_ids(ids, parametersets, self.function.__name__) + id_maker = IdMaker( + argnames, + parametersets, + idfn, + ids_, + self.config, + nodeid=nodeid, + func_name=self.function.__name__, + ) + return id_maker.make_unique_parameterset_ids() def _validate_ids( self, - ids: Iterable[Union[None, str, float, int, bool]], - parameters: Sequence[ParameterSet], + ids: Iterable[Optional[object]], + parametersets: Sequence[ParameterSet], func_name: str, - ) -> List[Union[None, str]]: + ) -> List[Optional[object]]: try: num_ids = len(ids) # type: ignore[arg-type] except TypeError: @@ -1218,69 +1372,53 @@ class Metafunc: iter(ids) except TypeError as e: raise TypeError("ids must be a callable or an iterable") from e - num_ids = len(parameters) + num_ids = len(parametersets) # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849 - if num_ids != len(parameters) and num_ids != 0: + if num_ids != len(parametersets) and num_ids != 0: msg = "In {}: {} parameter sets specified, with different number of ids: {}" - fail(msg.format(func_name, len(parameters), num_ids), pytrace=False) - - new_ids = [] - for idx, id_value in enumerate(itertools.islice(ids, num_ids)): - if id_value is None or isinstance(id_value, str): - new_ids.append(id_value) - elif isinstance(id_value, (float, int, bool)): - new_ids.append(str(id_value)) - else: - msg = ( # type: ignore[unreachable] - "In {}: ids must be list of string/float/int/bool, " - "found: {} (type: {!r}) at index {}" - ) - fail( - msg.format(func_name, saferepr(id_value), type(id_value), idx), - pytrace=False, - ) - return new_ids + fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False) + + return list(itertools.islice(ids, num_ids)) - def _resolve_arg_value_types( + def _resolve_args_directness( self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], - ) -> Dict[str, "Literal['params', 'funcargs']"]: - """Resolve if each parametrized argument must be considered a - parameter to a fixture or a "funcarg" to the function, based on the - ``indirect`` parameter of the parametrized() call. + ) -> Dict[str, Literal["indirect", "direct"]]: + """Resolve if each parametrized argument must be considered an indirect + parameter to a fixture of the same name, or a direct parameter to the + parametrized function, based on the ``indirect`` parameter of the + parametrized() call. - :param List[str] argnames: List of argument names passed to ``parametrize()``. - :param indirect: Same as the ``indirect`` parameter of ``parametrize()``. - :rtype: Dict[str, str] - A dict mapping each arg name to either: - * "params" if the argname should be the parameter of a fixture of the same name. - * "funcargs" if the argname should be a parameter to the parametrized test function. + :param argnames: + List of argument names passed to ``parametrize()``. + :param indirect: + Same as the ``indirect`` parameter of ``parametrize()``. + :returns + A dict mapping each arg name to either "indirect" or "direct". """ + arg_directness: Dict[str, Literal["indirect", "direct"]] if isinstance(indirect, bool): - valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys( - argnames, "params" if indirect else "funcargs" + arg_directness = dict.fromkeys( + argnames, "indirect" if indirect else "direct" ) elif isinstance(indirect, Sequence): - valtypes = dict.fromkeys(argnames, "funcargs") + arg_directness = dict.fromkeys(argnames, "direct") for arg in indirect: if arg not in argnames: fail( - "In {}: indirect fixture '{}' doesn't exist".format( - self.function.__name__, arg - ), + f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist", pytrace=False, ) - valtypes[arg] = "params" + arg_directness[arg] = "indirect" else: fail( - "In {func}: expected Sequence or boolean for indirect, got {type}".format( - type=type(indirect).__name__, func=self.function.__name__ - ), + f"In {self.function.__name__}: expected Sequence or boolean" + f" for indirect, got {type(indirect).__name__}", pytrace=False, ) - return valtypes + return arg_directness def _validate_if_using_arg_names( self, @@ -1299,9 +1437,7 @@ class Metafunc: if arg not in self.fixturenames: if arg in default_arg_names: fail( - "In {}: function already takes an argument '{}' with a default value".format( - func_name, arg - ), + f"In {func_name}: function already takes an argument '{arg}' with a default value", pytrace=False, ) else: @@ -1337,7 +1473,7 @@ def _find_parametrized_scope( if all_arguments_are_fixtures: fixturedefs = arg2fixturedefs or {} used_scopes = [ - fixturedef[0]._scope + fixturedef[-1]._scope for name, fixturedef in fixturedefs.items() if name in argnames ] @@ -1360,239 +1496,8 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) - return val if escape_option else ascii_escaped(val) # type: ignore -def _idval( - val: object, - argname: str, - idx: int, - idfn: Optional[Callable[[Any], Optional[object]]], - nodeid: Optional[str], - config: Optional[Config], -) -> str: - if idfn: - try: - generated_id = idfn(val) - if generated_id is not None: - val = generated_id - except Exception as e: - prefix = f"{nodeid}: " if nodeid is not None else "" - msg = "error raised while trying to determine id of parameter '{}' at position {}" - msg = prefix + msg.format(argname, idx) - raise ValueError(msg) from e - elif config: - hook_id: Optional[str] = config.hook.pytest_make_parametrize_id( - config=config, val=val, argname=argname - ) - if hook_id: - return hook_id - - if isinstance(val, STRING_TYPES): - return _ascii_escaped_by_config(val, config) - elif val is None or isinstance(val, (float, int, bool, complex)): - return str(val) - elif isinstance(val, Pattern): - return ascii_escaped(val.pattern) - elif val is NOTSET: - # Fallback to default. Note that NOTSET is an enum.Enum. - pass - elif isinstance(val, enum.Enum): - return str(val) - elif isinstance(getattr(val, "__name__", None), str): - # Name of a class, function, module, etc. - name: str = getattr(val, "__name__") - return name - return str(argname) + str(idx) - - -def _idvalset( - idx: int, - parameterset: ParameterSet, - argnames: Iterable[str], - idfn: Optional[Callable[[Any], Optional[object]]], - ids: Optional[List[Union[None, str]]], - nodeid: Optional[str], - config: Optional[Config], -) -> str: - if parameterset.id is not None: - return parameterset.id - id = None if ids is None or idx >= len(ids) else ids[idx] - if id is None: - this_id = [ - _idval(val, argname, idx, idfn, nodeid=nodeid, config=config) - for val, argname in zip(parameterset.values, argnames) - ] - return "-".join(this_id) - else: - return _ascii_escaped_by_config(id, config) - - -def idmaker( - argnames: Iterable[str], - parametersets: Iterable[ParameterSet], - idfn: Optional[Callable[[Any], Optional[object]]] = None, - ids: Optional[List[Union[None, str]]] = None, - config: Optional[Config] = None, - nodeid: Optional[str] = None, -) -> List[str]: - resolved_ids = [ - _idvalset( - valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid - ) - for valindex, parameterset in enumerate(parametersets) - ] - - # All IDs must be unique! - unique_ids = set(resolved_ids) - if len(unique_ids) != len(resolved_ids): - - # Record the number of occurrences of each test ID. - test_id_counts = Counter(resolved_ids) - - # Map the test ID to its next suffix. - test_id_suffixes: Dict[str, int] = defaultdict(int) - - # Suffix non-unique IDs to make them unique. - for index, test_id in enumerate(resolved_ids): - if test_id_counts[test_id] > 1: - resolved_ids[index] = f"{test_id}{test_id_suffixes[test_id]}" - test_id_suffixes[test_id] += 1 - - return resolved_ids - - -def _pretty_fixture_path(func) -> str: - cwd = Path.cwd() - loc = Path(getlocation(func, str(cwd))) - prefix = Path("...", "_pytest") - try: - return str(prefix / loc.relative_to(_PYTEST_DIR)) - except ValueError: - return bestrelpath(cwd, loc) - - -def show_fixtures_per_test(config): - from _pytest.main import wrap_session - - return wrap_session(config, _show_fixtures_per_test) - - -def _show_fixtures_per_test(config: Config, session: Session) -> None: - import _pytest.config - - session.perform_collect() - curdir = Path.cwd() - tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") - - def get_best_relpath(func) -> str: - loc = getlocation(func, str(curdir)) - return bestrelpath(curdir, Path(loc)) - - def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: - argname = fixture_def.argname - if verbose <= 0 and argname.startswith("_"): - return - prettypath = _pretty_fixture_path(fixture_def.func) - tw.write(f"{argname}", green=True) - tw.write(f" -- {prettypath}", yellow=True) - tw.write("\n") - fixture_doc = inspect.getdoc(fixture_def.func) - if fixture_doc: - write_docstring( - tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc - ) - else: - tw.line(" no docstring available", red=True) - - def write_item(item: nodes.Item) -> None: - # Not all items have _fixtureinfo attribute. - info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) - if info is None or not info.name2fixturedefs: - # This test item does not use any fixtures. - return - tw.line() - tw.sep("-", f"fixtures used by {item.name}") - # TODO: Fix this type ignore. - tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined] - # dict key not used in loop but needed for sorting. - for _, fixturedefs in sorted(info.name2fixturedefs.items()): - assert fixturedefs is not None - if not fixturedefs: - continue - # Last item is expected to be the one used by the test item. - write_fixture(fixturedefs[-1]) - - for session_item in session.items: - write_item(session_item) - - -def showfixtures(config: Config) -> Union[int, ExitCode]: - from _pytest.main import wrap_session - - return wrap_session(config, _showfixtures_main) - - -def _showfixtures_main(config: Config, session: Session) -> None: - import _pytest.config - - session.perform_collect() - curdir = Path.cwd() - tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") - - fm = session._fixturemanager - - available = [] - seen: Set[Tuple[str, str]] = set() - - for argname, fixturedefs in fm._arg2fixturedefs.items(): - assert fixturedefs is not None - if not fixturedefs: - continue - for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, str(curdir)) - if (fixturedef.argname, loc) in seen: - continue - seen.add((fixturedef.argname, loc)) - available.append( - ( - len(fixturedef.baseid), - fixturedef.func.__module__, - _pretty_fixture_path(fixturedef.func), - fixturedef.argname, - fixturedef, - ) - ) - - available.sort() - currentmodule = None - for baseid, module, prettypath, argname, fixturedef in available: - if currentmodule != module: - if not module.startswith("_pytest."): - tw.line() - tw.sep("-", f"fixtures defined from {module}") - currentmodule = module - if verbose <= 0 and argname.startswith("_"): - continue - tw.write(f"{argname}", green=True) - if fixturedef.scope != "function": - tw.write(" [%s scope]" % fixturedef.scope, cyan=True) - tw.write(f" -- {prettypath}", yellow=True) - tw.write("\n") - doc = inspect.getdoc(fixturedef.func) - if doc: - write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) - else: - tw.line(" no docstring available", red=True) - tw.line() - - -def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: - for line in doc.split("\n"): - tw.line(indent + line) - - class Function(PyobjMixin, nodes.Item): - """An Item responsible for setting up and executing a Python test function. + """Item responsible for setting up and executing a Python test function. :param name: The full function name, including any decorations like those @@ -1602,7 +1507,7 @@ class Function(PyobjMixin, nodes.Item): :param config: The pytest Config object. :param callspec: - If given, this is function has been parametrized and the callspec contains + If given, this function has been parametrized and the callspec contains meta information about the parametrization. :param callobj: If given, the object which will be called when the Function is invoked, @@ -1630,7 +1535,7 @@ class Function(PyobjMixin, nodes.Item): config: Optional[Config] = None, callspec: Optional[CallSpec2] = None, callobj=NOTSET, - keywords=None, + keywords: Optional[Mapping[str, Any]] = None, session: Optional[Session] = None, fixtureinfo: Optional[FuncFixtureInfo] = None, originalname: Optional[str] = None, @@ -1638,7 +1543,8 @@ class Function(PyobjMixin, nodes.Item): super().__init__(name, parent, config=config, session=session) if callobj is not NOTSET: - self.obj = callobj + self._obj = callobj + self._instance = getattr(callobj, "__self__", None) #: Original function name, without any decorations (for example #: parametrization adds a ``"[...]"`` suffix to function names), used to access @@ -1651,60 +1557,68 @@ class Function(PyobjMixin, nodes.Item): # Note: when FunctionDefinition is introduced, we should change ``originalname`` # to a readonly property that returns FunctionDefinition.name. - self.keywords.update(self.obj.__dict__) self.own_markers.extend(get_unpacked_marks(self.obj)) if callspec: self.callspec = callspec - # this is total hostile and a mess - # keywords are broken by design by now - # this will be redeemed later - for mark in callspec.marks: - # feel free to cry, this was broken for years before - # and keywords can't fix it per design - self.keywords[mark.name] = mark - self.own_markers.extend(normalize_mark_list(callspec.marks)) - if keywords: - self.keywords.update(keywords) + self.own_markers.extend(callspec.marks) # todo: this is a hell of a hack # https://github.com/pytest-dev/pytest/issues/4569 - - self.keywords.update( - { - mark.name: True - for mark in self.iter_markers() - if mark.name not in self.keywords - } - ) + # Note: the order of the updates is important here; indicates what + # takes priority (ctor argument over function attributes over markers). + # Take own_markers only; NodeKeywords handles parent traversal on its own. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + self.keywords.update(self.obj.__dict__) + if keywords: + self.keywords.update(keywords) if fixtureinfo is None: - fixtureinfo = self.session._fixturemanager.getfixtureinfo( - self, self.obj, self.cls, funcargs=True - ) + fm = self.session._fixturemanager + fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls) self._fixtureinfo: FuncFixtureInfo = fixtureinfo self.fixturenames = fixtureinfo.names_closure self._initrequest() + # todo: determine sound type limitations @classmethod - def from_parent(cls, parent, **kw): # todo: determine sound type limitations + def from_parent(cls, parent, **kw) -> "Self": """The public constructor.""" return super().from_parent(parent=parent, **kw) def _initrequest(self) -> None: self.funcargs: Dict[str, object] = {} - self._request = fixtures.FixtureRequest(self, _ispytest=True) + self._request = fixtures.TopRequest(self, _ispytest=True) @property def function(self): """Underlying python 'function' object.""" return getimfunc(self.obj) - def _getobj(self): - assert self.parent is not None + @property + def instance(self): + try: + return self._instance + except AttributeError: + if isinstance(self.parent, Class): + # Each Function gets a fresh class instance. + self._instance = self._getinstance() + else: + self._instance = None + return self._instance + + def _getinstance(self): if isinstance(self.parent, Class): # Each Function gets a fresh class instance. - parent_obj = self.parent.newinstance() + return self.parent.newinstance() + else: + return None + + def _getobj(self): + instance = self.instance + if instance is not None: + parent_obj = instance else: + assert self.parent is not None parent_obj = self.parent.obj # type: ignore[attr-defined] return getattr(parent_obj, self.originalname) @@ -1720,7 +1634,7 @@ class Function(PyobjMixin, nodes.Item): def setup(self) -> None: self._request._fillfixtures() - def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: + def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code.from_function(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno @@ -1732,14 +1646,22 @@ class Function(PyobjMixin, nodes.Item): ntraceback = ntraceback.filter(filter_traceback) if not ntraceback: ntraceback = traceback + ntraceback = ntraceback.filter(excinfo) - excinfo.traceback = ntraceback.filter() # issue364: mark all but first and last frames to # only show a single-line message for each frame. if self.config.getoption("tbstyle", "auto") == "auto": - if len(excinfo.traceback) > 2: - for entry in excinfo.traceback[1:-1]: - entry.set_repr_style("short") + if len(ntraceback) > 2: + ntraceback = Traceback( + ( + ntraceback[0], + *(t.with_repr_style("short") for t in ntraceback[1:-1]), + ntraceback[-1], + ) + ) + + return ntraceback + return excinfo.traceback # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] @@ -1753,10 +1675,8 @@ class Function(PyobjMixin, nodes.Item): class FunctionDefinition(Function): - """ - This class is a step gap solution until we evolve to have actual function definition nodes - and manage to get rid of ``metafunc``. - """ + """This class is a stop gap solution until we evolve to have actual function + definition nodes and manage to get rid of ``metafunc``.""" def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python_api.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python_api.py index cb72fde1e1f..7d89fdd809e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python_api.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/python_api.py @@ -1,14 +1,16 @@ -import math -import pprint +# mypy: allow-untyped-defs +from collections.abc import Collection from collections.abc import Sized from decimal import Decimal +import math from numbers import Complex +import pprint from types import TracebackType from typing import Any from typing import Callable from typing import cast -from typing import Generic -from typing import Iterable +from typing import ContextManager +from typing import final from typing import List from typing import Mapping from typing import Optional @@ -21,23 +23,12 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -if TYPE_CHECKING: - from numpy import ndarray - - import _pytest._code -from _pytest.compat import final -from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail -def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: - at_str = f" at {at}" if at else "" - return TypeError( - "cannot make approximate comparisons to non-numeric values: {!r} {}".format( - value, at_str - ) - ) +if TYPE_CHECKING: + from numpy import ndarray def _compare_approx( @@ -131,12 +122,13 @@ class ApproxBase: # a numeric type. For this reason, the default is to do nothing. The # classes that deal with sequences should reimplement this method to # raise if there are any non-numeric elements in the sequence. - pass -def _recursive_list_map(f, x): - if isinstance(x, list): - return [_recursive_list_map(f, xi) for xi in x] +def _recursive_sequence_map(f, x): + """Recursively map a function over a sequence of arbitrary depth""" + if isinstance(x, (list, tuple)): + seq_type = type(x) + return seq_type(_recursive_sequence_map(f, xi) for xi in x) else: return f(x) @@ -145,10 +137,12 @@ class ApproxNumpy(ApproxBase): """Perform approximate comparisons where the expected value is numpy array.""" def __repr__(self) -> str: - list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) + list_scalars = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) return f"approx({list_scalars!r})" - def _repr_compare(self, other_side: "ndarray") -> List[str]: + def _repr_compare(self, other_side: Union["ndarray", List[Any]]) -> List[str]: import itertools import math @@ -165,14 +159,18 @@ class ApproxNumpy(ApproxBase): return value np_array_shape = self.expected.shape - approx_side_as_list = _recursive_list_map( + approx_side_as_seq = _recursive_sequence_map( self._approx_scalar, self.expected.tolist() ) - if np_array_shape != other_side.shape: + # convert other_side to numpy array to ensure shape attribute is available + other_side_as_array = _as_numpy_array(other_side) + assert other_side_as_array is not None + + if np_array_shape != other_side_as_array.shape: return [ "Impossible to compare arrays with different shapes.", - f"Shapes: {np_array_shape} and {other_side.shape}", + f"Shapes: {np_array_shape} and {other_side_as_array.shape}", ] number_of_elements = self.expected.size @@ -180,8 +178,8 @@ class ApproxNumpy(ApproxBase): max_rel_diff = -math.inf different_ids = [] for index in itertools.product(*(range(i) for i in np_array_shape)): - approx_value = get_value_from_nested_list(approx_side_as_list, index) - other_value = get_value_from_nested_list(other_side, index) + approx_value = get_value_from_nested_list(approx_side_as_seq, index) + other_value = get_value_from_nested_list(other_side_as_array, index) if approx_value != other_value: abs_diff = abs(approx_value.expected - other_value) max_abs_diff = max(max_abs_diff, abs_diff) @@ -194,8 +192,8 @@ class ApproxNumpy(ApproxBase): message_data = [ ( str(index), - str(get_value_from_nested_list(other_side, index)), - str(get_value_from_nested_list(approx_side_as_list, index)), + str(get_value_from_nested_list(other_side_as_array, index)), + str(get_value_from_nested_list(approx_side_as_seq, index)), ) for index in different_ids ] @@ -244,9 +242,7 @@ class ApproxMapping(ApproxBase): with numeric values (the keys can be anything).""" def __repr__(self) -> str: - return "approx({!r})".format( - {k: self._approx_scalar(v) for k, v in self.expected.items()} - ) + return f"approx({({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})" def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]: import math @@ -263,13 +259,20 @@ class ApproxMapping(ApproxBase): approx_side_as_map.items(), other_side.values() ): if approx_value != other_value: - max_abs_diff = max( - max_abs_diff, abs(approx_value.expected - other_value) - ) - max_rel_diff = max( - max_rel_diff, - abs((approx_value.expected - other_value) / approx_value.expected), - ) + if approx_value.expected is not None and other_value is not None: + max_abs_diff = max( + max_abs_diff, abs(approx_value.expected - other_value) + ) + if approx_value.expected == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) different_ids.append(approx_key) message_data = [ @@ -307,20 +310,17 @@ class ApproxMapping(ApproxBase): raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) -class ApproxSequencelike(ApproxBase): +class ApproxSequenceLike(ApproxBase): """Perform approximate comparisons where the expected value is a sequence of numbers.""" def __repr__(self) -> str: seq_type = type(self.expected) - if seq_type not in (tuple, list, set): + if seq_type not in (tuple, list): seq_type = list - return "approx({!r})".format( - seq_type(self._approx_scalar(x) for x in self.expected) - ) + return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})" def _repr_compare(self, other_side: Sequence[float]) -> List[str]: import math - import numpy as np if len(self.expected) != len(other_side): return [ @@ -328,7 +328,7 @@ class ApproxSequencelike(ApproxBase): f"Lengths: {len(self.expected)} and {len(other_side)}", ] - approx_side_as_map = _recursive_list_map(self._approx_scalar, self.expected) + approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected) number_of_elements = len(approx_side_as_map) max_abs_diff = -math.inf @@ -341,7 +341,7 @@ class ApproxSequencelike(ApproxBase): abs_diff = abs(approx_value.expected - other_value) max_abs_diff = max(max_abs_diff, abs_diff) if other_value == 0.0: - max_rel_diff = np.inf + max_rel_diff = math.inf else: max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value)) different_ids.append(i) @@ -397,7 +397,7 @@ class ApproxScalar(ApproxBase): # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( - abs(self.expected) # type: ignore[arg-type] + abs(self.expected) ): return str(self.expected) @@ -441,8 +441,8 @@ class ApproxScalar(ApproxBase): # Allow the user to control whether NaNs are considered equal to each # other or not. The abs() calls are for compatibility with complex # numbers. - if math.isnan(abs(self.expected)): # type: ignore[arg-type] - return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type] + if math.isnan(abs(self.expected)): + return self.nan_ok and math.isnan(abs(actual)) # Infinity shouldn't be approximately equal to anything but itself, but # if there's a relative tolerance, it will be infinite and infinity @@ -450,11 +450,11 @@ class ApproxScalar(ApproxBase): # case would have been short circuited above, so here we can just # return false if the expected value is infinite. The abs() call is # for compatibility with complex numbers. - if math.isinf(abs(self.expected)): # type: ignore[arg-type] + if math.isinf(abs(self.expected)): return False # Return true if the two numbers are within the tolerance. - result: bool = abs(self.expected - actual) <= self.tolerance + result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type] return result # Ignore type because of https://github.com/python/mypy/issues/4266. @@ -516,10 +516,10 @@ class ApproxDecimal(ApproxScalar): def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: - """Assert that two numbers (or two sets of numbers) are equal to each other + """Assert that two numbers (or two ordered sequences of numbers) are equal to each other within some tolerance. - Due to the :std:doc:`tutorial/floatingpoint`, numbers that we + Due to the :doc:`python:tutorial/floatingpoint`, numbers that we would intuitively expect to be equal are not always so:: >>> 0.1 + 0.2 == 0.3 @@ -548,16 +548,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: >>> 0.1 + 0.2 == approx(0.3) True - The same syntax also works for sequences of numbers:: + The same syntax also works for ordered sequences of numbers:: >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) True - Dictionary *values*:: - - >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) - True - ``numpy`` arrays:: >>> import numpy as np # doctest: +SKIP @@ -570,6 +565,20 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP True + Only ordered sequences are supported, because ``approx`` needs + to infer the relative position of the sequences without ambiguity. This means + ``sets`` and other unordered sequences are not supported. + + Finally, dictionary *values* can also be compared:: + + >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) + True + + The comparison will be true if both mappings have the same keys and their + respective values match the expected tolerances. + + **Tolerances** + By default, ``approx`` considers numbers within a relative tolerance of ``1e-6`` (i.e. one part in a million) of its expected value to be equal. This treatment would lead to surprising results if the expected value was @@ -659,6 +668,11 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: specialised test helpers in :std:doc:`numpy:reference/routines.testing` if you need support for comparisons, NaNs, or ULP-based tolerances. + To match strings using regex, you can use + `Matches <https://github.com/asottile/re-assert#re_assertmatchespattern-str-args-kwargs>`_ + from the + `re_assert package <https://github.com/asottile/re-assert>`_. + .. warning:: .. versionchanged:: 3.2 @@ -683,7 +697,6 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: ``approx`` falls back to strict equality for nonnumeric types instead of raising ``TypeError``. """ - # Delegate the comparison to a class that knows how to deal with the type # of the expected value (e.g. int, float, list, dict, numpy.array, etc). # @@ -709,12 +722,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: expected = _as_numpy_array(expected) cls = ApproxNumpy elif ( - isinstance(expected, Iterable) + hasattr(expected, "__getitem__") and isinstance(expected, Sized) - # Type ignored because the error is wrong -- not unreachable. - and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] + and not isinstance(expected, (str, bytes)) ): - cls = ApproxSequencelike + cls = ApproxSequenceLike + elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)): + msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" + raise TypeError(msg) else: cls = ApproxScalar @@ -758,8 +773,7 @@ def raises( expected_exception: Union[Type[E], Tuple[Type[E], ...]], *, match: Optional[Union[str, Pattern[str]]] = ..., -) -> "RaisesContext[E]": - ... +) -> "RaisesContext[E]": ... @overload @@ -768,38 +782,41 @@ def raises( func: Callable[..., Any], *args: Any, **kwargs: Any, -) -> _pytest._code.ExceptionInfo[E]: - ... +) -> _pytest._code.ExceptionInfo[E]: ... def raises( expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any ) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]: - r"""Assert that a code block/function call raises ``expected_exception`` - or raise a failure exception otherwise. + r"""Assert that a code block/function call raises an exception type, or one of its subclasses. + + :param expected_exception: + The expected exception type, or a tuple if one of multiple possible + exception types are expected. Note that subclasses of the passed exceptions + will also match. - :kwparam match: + :kwparam str | re.Pattern[str] | None match: If specified, a string containing a regular expression, or a regular expression object, that is tested against the string - representation of the exception using :py:func:`re.search`. To match a literal - string that may contain :std:ref:`special characters <re-syntax>`, the pattern can - first be escaped with :py:func:`re.escape`. + representation of the exception and its :pep:`678` `__notes__` + using :func:`re.search`. - (This is only used when :py:func:`pytest.raises` is used as a context manager, + To match a literal string that may contain :ref:`special characters + <re-syntax>`, the pattern can first be escaped with :func:`re.escape`. + + (This is only used when ``pytest.raises`` is used as a context manager, and passed through to the function otherwise. - When using :py:func:`pytest.raises` as a function, you can use: + When using ``pytest.raises`` as a function, you can use: ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.) - .. currentmodule:: _pytest._code - Use ``pytest.raises`` as a context manager, which will capture the exception of the given - type:: + type, or any of its subclasses:: >>> import pytest >>> with pytest.raises(ZeroDivisionError): ... 1/0 - If the code block does not raise the expected exception (``ZeroDivisionError`` in the example + If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example above), or no exception at all, the check will fail instead. You can also use the keyword argument ``match`` to assert that the @@ -811,6 +828,14 @@ def raises( >>> with pytest.raises(ValueError, match=r'must be \d+$'): ... raise ValueError("value must be 42") + The ``match`` argument searches the formatted exception string, which includes any + `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``: + + >>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP + ... e = ValueError("value must be 42") + ... e.add_note("had a note added") + ... raise e + The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the details of the captured exception:: @@ -819,6 +844,20 @@ def raises( >>> assert exc_info.type is ValueError >>> assert exc_info.value.args[0] == "value must be 42" + .. warning:: + + Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this:: + + with pytest.raises(Exception): # Careful, this will catch ANY exception raised. + some_function() + + Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide + real bugs, where the user wrote this expecting a specific exception, but some other exception is being + raised due to a bug introduced during a refactoring. + + Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch + **any** exception raised. + .. note:: When using ``pytest.raises`` as a context manager, it's worthwhile to @@ -831,7 +870,7 @@ def raises( >>> with pytest.raises(ValueError) as exc_info: ... if value > 10: ... raise ValueError("value must be <= 10") - ... assert exc_info.type is ValueError # this will not execute + ... assert exc_info.type is ValueError # This will not execute. Instead, the following approach must be taken (note the difference in scope):: @@ -850,6 +889,10 @@ def raises( See :ref:`parametrizing_conditional_raising` for an example. + .. seealso:: + + :ref:`assertraises` for more examples and detailed discussion. + **Legacy form** It is possible to specify a callable by passing a to-be-called lambda:: @@ -885,11 +928,17 @@ def raises( """ __tracebackhide__ = True + if not expected_exception: + raise ValueError( + f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. " + f"Raising exceptions is already understood as failing the test, so you don't need " + f"any special code to say 'this should never raise an exception'." + ) if isinstance(expected_exception, type): - excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,) + expected_exceptions: Tuple[Type[E], ...] = (expected_exception,) else: - excepted_exceptions = expected_exception - for exc in excepted_exceptions: + expected_exceptions = expected_exception + for exc in expected_exceptions: if not isinstance(exc, type) or not issubclass(exc, BaseException): msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable] not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ @@ -912,11 +961,7 @@ def raises( try: func(*args[1:], **kwargs) except expected_exception as e: - # We just caught the exception - there is a traceback. - assert e.__traceback__ is not None - return _pytest._code.ExceptionInfo.from_exc_info( - (type(e), e, e.__traceback__) - ) + return _pytest._code.ExceptionInfo.from_exception(e) fail(message) @@ -925,7 +970,7 @@ raises.Exception = fail.Exception # type: ignore @final -class RaisesContext(Generic[E]): +class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]): def __init__( self, expected_exception: Union[Type[E], Tuple[Type[E], ...]], diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/recwarn.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/recwarn.py index 175b571a80c..63e7a4bd6dc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/recwarn.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/recwarn.py @@ -1,9 +1,12 @@ +# mypy: allow-untyped-defs """Record warnings during test function execution.""" + +from pprint import pformat import re -import warnings from types import TracebackType from typing import Any from typing import Callable +from typing import final from typing import Generator from typing import Iterator from typing import List @@ -14,11 +17,11 @@ from typing import Tuple from typing import Type from typing import TypeVar from typing import Union +import warnings -from _pytest.compat import final from _pytest.deprecated import check_ispytest -from _pytest.deprecated import WARNS_NONE_ARG from _pytest.fixtures import fixture +from _pytest.outcomes import Exit from _pytest.outcomes import fail @@ -29,7 +32,7 @@ T = TypeVar("T") def recwarn() -> Generator["WarningsRecorder", None, None]: """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. - See https://docs.python.org/library/how-to/capture-warnings.html for information + See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information on warning categories. """ wrec = WarningsRecorder(_ispytest=True) @@ -41,19 +44,17 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: @overload def deprecated_call( *, match: Optional[Union[str, Pattern[str]]] = ... -) -> "WarningsRecorder": - ... +) -> "WarningsRecorder": ... @overload -def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: - ... +def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ... def deprecated_call( func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any ) -> Union["WarningsRecorder", Any]: - """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``. + """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``. This function can be used as a context manager:: @@ -78,8 +79,10 @@ def deprecated_call( """ __tracebackhide__ = True if func is not None: - args = (func,) + args - return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) + args = (func, *args) + return warns( + (DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs + ) @overload @@ -87,8 +90,7 @@ def warns( expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., *, match: Optional[Union[str, Pattern[str]]] = ..., -) -> "WarningsChecker": - ... +) -> "WarningsChecker": ... @overload @@ -97,8 +99,7 @@ def warns( func: Callable[..., T], *args: Any, **kwargs: Any, -) -> T: - ... +) -> T: ... def warns( @@ -109,15 +110,15 @@ def warns( ) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. - Specifically, the parameter ``expected_warning`` can be a warning class or - sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or - classes. + Specifically, the parameter ``expected_warning`` can be a warning class or tuple + of warning classes, and the code inside the ``with`` block must issue at least one + warning of that class or classes. - This helper produces a list of :class:`warnings.WarningMessage` objects, - one for each warning raised. + This helper produces a list of :class:`warnings.WarningMessage` objects, one for + each warning emitted (regardless of whether it is an ``expected_warning`` or not). + Since pytest 8.0, unmatched warnings are also re-emitted when the context closes. - This function can be used as a context manager, or any of the other ways - :func:`pytest.raises` can be used:: + This function can be used as a context manager:: >>> import pytest >>> with pytest.warns(RuntimeWarning): @@ -132,20 +133,30 @@ def warns( >>> with pytest.warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("value must be 42", UserWarning) - >>> with pytest.warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("this is not here", UserWarning) + >>> with pytest.warns(UserWarning): # catch re-emitted warning + ... with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) Traceback (most recent call last): ... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... + **Using with** ``pytest.mark.parametrize`` + + When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests + such that some runs raise a warning and others do not. + + This could be achieved in the same way as with exceptions, see + :ref:`parametrizing_conditional_raising` for an example. + """ __tracebackhide__ = True if not args: if kwargs: - msg = "Unexpected keyword arguments passed to pytest.warns: " - msg += ", ".join(sorted(kwargs)) - msg += "\nUse context-manager form instead?" - raise TypeError(msg) + argnames = ", ".join(sorted(kwargs)) + raise TypeError( + f"Unexpected keyword arguments passed to pytest.warns: {argnames}" + "\nUse context-manager form instead?" + ) return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) else: func = args[0] @@ -155,16 +166,22 @@ def warns( return func(*args[1:], **kwargs) -class WarningsRecorder(warnings.catch_warnings): +class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] """A context manager to record raised warnings. + Each recorded warning is an instance of :class:`warnings.WarningMessage`. + Adapted from `warnings.catch_warnings`. + + .. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + """ def __init__(self, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - # Type ignored due to the way typeshed handles warnings.catch_warnings. - super().__init__(record=True) # type: ignore[call-arg] + super().__init__(record=True) self._entered = False self._list: List[warnings.WarningMessage] = [] @@ -186,12 +203,23 @@ class WarningsRecorder(warnings.catch_warnings): return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning, raise exception if not exists.""" + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + """ + best_idx: Optional[int] = None for i, w in enumerate(self._list): - if issubclass(w.category, cls): - return self._list.pop(i) + if w.category == cls: + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) + ): + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) __tracebackhide__ = True - raise AssertionError("%r not found in warning list" % cls) + raise AssertionError(f"{cls!r} not found in warning list") def clear(self) -> None: """Clear the list of recorded warnings.""" @@ -202,7 +230,7 @@ class WarningsRecorder(warnings.catch_warnings): def __enter__(self) -> "WarningsRecorder": # type: ignore if self._entered: __tracebackhide__ = True - raise RuntimeError("Cannot enter %r twice" % self) + raise RuntimeError(f"Cannot enter {self!r} twice") _list = super().__enter__() # record=True means it's None. assert _list is not None @@ -218,7 +246,7 @@ class WarningsRecorder(warnings.catch_warnings): ) -> None: if not self._entered: __tracebackhide__ = True - raise RuntimeError("Cannot exit %r without entering first" % self) + raise RuntimeError(f"Cannot exit {self!r} without entering first") super().__exit__(exc_type, exc_val, exc_tb) @@ -231,9 +259,7 @@ class WarningsRecorder(warnings.catch_warnings): class WarningsChecker(WarningsRecorder): def __init__( self, - expected_warning: Optional[ - Union[Type[Warning], Tuple[Type[Warning], ...]] - ] = Warning, + expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning, match_expr: Optional[Union[str, Pattern[str]]] = None, *, _ispytest: bool = False, @@ -242,15 +268,14 @@ class WarningsChecker(WarningsRecorder): super().__init__(_ispytest=True) msg = "exceptions must be derived from Warning, not %s" - if expected_warning is None: - warnings.warn(WARNS_NONE_ARG, stacklevel=4) - expected_warning_tup = None - elif isinstance(expected_warning, tuple): + if isinstance(expected_warning, tuple): for exc in expected_warning: if not issubclass(exc, Warning): raise TypeError(msg % type(exc)) expected_warning_tup = expected_warning - elif issubclass(expected_warning, Warning): + elif isinstance(expected_warning, type) and issubclass( + expected_warning, Warning + ): expected_warning_tup = (expected_warning,) else: raise TypeError(msg % type(expected_warning)) @@ -258,6 +283,12 @@ class WarningsChecker(WarningsRecorder): self.expected_warning = expected_warning_tup self.match_expr = match_expr + def matches(self, warning: warnings.WarningMessage) -> bool: + assert self.expected_warning is not None + return issubclass(warning.category, self.expected_warning) and bool( + self.match_expr is None or re.search(self.match_expr, str(warning.message)) + ) + def __exit__( self, exc_type: Optional[Type[BaseException]], @@ -268,29 +299,68 @@ class WarningsChecker(WarningsRecorder): __tracebackhide__ = True - # only check if we're not currently handling an exception - if exc_type is None and exc_val is None and exc_tb is None: - if self.expected_warning is not None: - if not any(issubclass(r.category, self.expected_warning) for r in self): - __tracebackhide__ = True - fail( - "DID NOT WARN. No warnings of type {} were emitted. " - "The list of emitted warnings is: {}.".format( - self.expected_warning, [each.message for each in self] - ) + # BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within + # pytest.warns should *not* trigger "DID NOT WARN" and get suppressed + # when the warning doesn't happen. Control-flow exceptions should always + # propagate. + if exc_val is not None and ( + not isinstance(exc_val, Exception) + # Exit is an Exception, not a BaseException, for some reason. + or isinstance(exc_val, Exit) + ): + return + + def found_str() -> str: + return pformat([record.message for record in self], indent=2) + + try: + if not any(issubclass(w.category, self.expected_warning) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f" Emitted warnings: {found_str()}." + ) + elif not any(self.matches(w) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n" + f" Regex: {self.match_expr}\n" + f" Emitted warnings: {found_str()}." + ) + finally: + # Whether or not any warnings matched, we want to re-emit all unmatched warnings. + for w in self: + if not self.matches(w): + warnings.warn_explicit( + message=w.message, + category=w.category, + filename=w.filename, + lineno=w.lineno, + module=w.__module__, + source=w.source, ) - elif self.match_expr is not None: - for r in self: - if issubclass(r.category, self.expected_warning): - if re.compile(self.match_expr).search(str(r.message)): - break - else: - fail( - "DID NOT WARN. No warnings of type {} matching" - " ('{}') were emitted. The list of emitted warnings" - " is: {}.".format( - self.expected_warning, - self.match_expr, - [each.message for each in self], - ) - ) + + # Currently in Python it is possible to pass other types than an + # `str` message when creating `Warning` instances, however this + # causes an exception when :func:`warnings.filterwarnings` is used + # to filter those warnings. See + # https://github.com/python/cpython/issues/103577 for a discussion. + # While this can be considered a bug in CPython, we put guards in + # pytest as the error message produced without this check in place + # is confusing (#10865). + for w in self: + if type(w.message) is not UserWarning: + # If the warning was of an incorrect type then `warnings.warn()` + # creates a UserWarning. Any other warning must have been specified + # explicitly. + continue + if not w.message.args: + # UserWarning() without arguments must have been specified explicitly. + continue + msg = w.message.args[0] + if isinstance(msg, str): + continue + # It's possible that UserWarning was explicitly specified, and + # its first argument was not a string. But that case can't be + # distinguished from an invalid type. + raise TypeError( + f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})" + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/reports.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/reports.py index a68e68bc526..70f3212ce7b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/reports.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/reports.py @@ -1,12 +1,18 @@ -import os +# mypy: allow-untyped-defs +import dataclasses from io import StringIO +import os from pprint import pprint from typing import Any from typing import cast from typing import Dict +from typing import final from typing import Iterable from typing import Iterator from typing import List +from typing import Literal +from typing import Mapping +from typing import NoReturn from typing import Optional from typing import Tuple from typing import Type @@ -14,8 +20,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr @@ -28,16 +32,13 @@ from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import skip -if TYPE_CHECKING: - from typing import NoReturn - from typing_extensions import Literal +if TYPE_CHECKING: from _pytest.runner import CallInfo @@ -46,7 +47,7 @@ def getworkerinfoline(node): return node._workerinfocache except AttributeError: d = node.workerinfo - ver = "%s.%s.%s" % d["version_info"][:3] + ver = "{}.{}.{}".format(*d["version_info"][:3]) node._workerinfocache = s = "[{}] {} -- Python {} {}".format( d["id"], d["sysplatform"], ver, d["executable"] ) @@ -64,15 +65,14 @@ class BaseReport: ] sections: List[Tuple[str, str]] nodeid: str - outcome: "Literal['passed', 'failed', 'skipped']" + outcome: Literal["passed", "failed", "skipped"] def __init__(self, **kw: Any) -> None: self.__dict__.update(kw) if TYPE_CHECKING: # Can have arbitrary fields given to __init__(). - def __getattr__(self, key: str) -> Any: - ... + def __getattr__(self, key: str) -> Any: ... def toterminal(self, out: TerminalWriter) -> None: if hasattr(self, "node"): @@ -228,7 +228,7 @@ class BaseReport: def _report_unserialization_failure( type_name: str, report_class: Type[BaseReport], reportdict -) -> "NoReturn": +) -> NoReturn: url = "https://github.com/pytest-dev/pytest/issues" stream = StringIO() pprint("-" * 100, stream=stream) @@ -249,19 +249,24 @@ class TestReport(BaseReport): """ __test__ = False + # Defined by skipping plugin. + # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish. + wasxfail: str def __init__( self, nodeid: str, location: Tuple[str, Optional[int], str], - keywords, - outcome: "Literal['passed', 'failed', 'skipped']", + keywords: Mapping[str, Any], + outcome: Literal["passed", "failed", "skipped"], longrepr: Union[ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr ], - when: "Literal['setup', 'call', 'teardown']", + when: Literal["setup", "call", "teardown"], sections: Iterable[Tuple[str, str]] = (), duration: float = 0, + start: float = 0, + stop: float = 0, user_properties: Optional[Iterable[Tuple[str, object]]] = None, **extra, ) -> None: @@ -271,11 +276,13 @@ class TestReport(BaseReport): #: A (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. + #: The filesystempath may be relative to ``config.rootdir``. + #: The line number is 0-based. self.location: Tuple[str, Optional[int], str] = location #: A name -> value dictionary containing all keywords and #: markers associated with a test invocation. - self.keywords = keywords + self.keywords: Mapping[str, Any] = keywords #: Test outcome, always one of "passed", "failed", "skipped". self.outcome = outcome @@ -297,22 +304,31 @@ class TestReport(BaseReport): self.sections = list(sections) #: Time it took to run just the test. - self.duration = duration + self.duration: float = duration + + #: The system time when the call started, in seconds since the epoch. + self.start: float = start + #: The system time when the call ended, in seconds since the epoch. + self.stop: float = stop self.__dict__.update(extra) def __repr__(self) -> str: - return "<{} {!r} when={!r} outcome={!r}>".format( - self.__class__.__name__, self.nodeid, self.when, self.outcome - ) + return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>" @classmethod def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": - """Create and fill a TestReport with standard item and call info.""" + """Create and fill a TestReport with standard item and call info. + + :param item: The item. + :param call: The call info. + """ when = call.when # Remove "collect" from the Literal type -- only for collection calls. assert when != "collect" duration = call.duration + start = call.start + stop = call.stop keywords = {x: 1 for x in item.keywords} excinfo = call.excinfo sections = [] @@ -332,6 +348,9 @@ class TestReport(BaseReport): elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() + assert ( + r is not None + ), "There should always be a traceback entry for skipping a test." if excinfo.value._use_item_location: path, line = item.reportinfo()[:2] assert line is not None @@ -357,6 +376,8 @@ class TestReport(BaseReport): when, sections, duration, + start, + stop, user_properties=item.user_properties, ) @@ -402,13 +423,13 @@ class CollectReport(BaseReport): self.__dict__.update(extra) @property - def location(self): + def location( # type:ignore[override] + self, + ) -> Optional[Tuple[str, Optional[int], str]]: return (self.fspath, None, self.fspath) def __repr__(self) -> str: - return "<CollectReport {!r} lenresult={} outcome={!r}>".format( - self.nodeid, len(self.result), self.outcome - ) + return f"<CollectReport {self.nodeid!r} lenresult={len(self.result)} outcome={self.outcome!r}>" class CollectErrorRepr(TerminalRepr): @@ -420,7 +441,7 @@ class CollectErrorRepr(TerminalRepr): def pytest_report_to_serializable( - report: Union[CollectReport, TestReport] + report: Union[CollectReport, TestReport], ) -> Optional[Dict[str, Any]]: if isinstance(report, (TestReport, CollectReport)): data = report._to_json() @@ -452,17 +473,17 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: """ def serialize_repr_entry( - entry: Union[ReprEntry, ReprEntryNative] + entry: Union[ReprEntry, ReprEntryNative], ) -> Dict[str, Any]: - data = attr.asdict(entry) + data = dataclasses.asdict(entry) for key, value in data.items(): if hasattr(value, "__dict__"): - data[key] = attr.asdict(value) + data[key] = dataclasses.asdict(value) entry_data = {"type": type(entry).__name__, "data": data} return entry_data def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: - result = attr.asdict(reprtraceback) + result = dataclasses.asdict(reprtraceback) result["reprentries"] = [ serialize_repr_entry(x) for x in reprtraceback.reprentries ] @@ -472,7 +493,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: reprcrash: Optional[ReprFileLocation], ) -> Optional[Dict[str, Any]]: if reprcrash is not None: - return attr.asdict(reprcrash) + return dataclasses.asdict(reprcrash) else: return None @@ -568,7 +589,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: and "reprcrash" in reportdict["longrepr"] and "reprtraceback" in reportdict["longrepr"] ): - reprtraceback = deserialize_repr_traceback( reportdict["longrepr"]["reprtraceback"] ) @@ -585,11 +605,14 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: description, ) ) - exception_info: Union[ - ExceptionChainRepr, ReprExceptionInfo - ] = ExceptionChainRepr(chain) + exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = ( + ExceptionChainRepr(chain) + ) else: - exception_info = ReprExceptionInfo(reprtraceback, reprcrash) + exception_info = ReprExceptionInfo( + reprtraceback=reprtraceback, + reprcrash=reprcrash, + ) for section in reportdict["longrepr"]["sections"]: exception_info.addsection(*section) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/runner.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/runner.py index e43dd2dc818..d15a682f979 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/runner.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/runner.py @@ -1,13 +1,17 @@ +# mypy: allow-untyped-defs """Basic collect and runtest protocol implementations.""" + import bdb +import dataclasses import os import sys -import warnings from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generic from typing import List +from typing import Literal from typing import Optional from typing import Tuple from typing import Type @@ -15,8 +19,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - from .reports import BaseReport from .reports import CollectErrorRepr from .reports import CollectReport @@ -25,11 +27,10 @@ from _pytest import timing from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest.compat import final from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest -from _pytest.deprecated import UNITTEST_SKIP_DURING_COLLECTION from _pytest.nodes import Collector +from _pytest.nodes import Directory from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.outcomes import Exit @@ -37,9 +38,11 @@ from _pytest.outcomes import OutcomeException from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME -if TYPE_CHECKING: - from typing_extensions import Literal +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +if TYPE_CHECKING: from _pytest.main import Session from _pytest.terminal import TerminalReporter @@ -48,14 +51,14 @@ if TYPE_CHECKING: def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting", "reporting", after="general") + group = parser.getgroup("terminal reporting", "Reporting", after="general") group.addoption( "--durations", action="store", type=int, default=None, metavar="N", - help="show N slowest setup/test durations (N=0 for all).", + help="Show N slowest setup/test durations (N=0 for all)", ) group.addoption( "--durations-min", @@ -63,7 +66,8 @@ def pytest_addoption(parser: Parser) -> None: type=float, default=0.005, metavar="N", - help="Minimal duration in seconds for inclusion in slowest list. Default 0.005", + help="Minimal duration in seconds for inclusion in slowest list. " + "Default: 0.005.", ) @@ -81,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: dlist.append(rep) if not dlist: return - dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] + dlist.sort(key=lambda x: x.duration, reverse=True) if not durations: tr.write_sep("=", "slowest durations") else: @@ -92,8 +96,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: if verbose < 2 and rep.duration < durations_min: tr.write_line("") tr.write_line( - "(%s durations < %gs hidden. Use -vv to show these durations.)" - % (len(dlist) - i, durations_min) + f"({len(dlist) - i} durations < {durations_min:g}s hidden. Use -vv to show these durations.)" ) break tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") @@ -162,6 +165,8 @@ def pytest_runtest_call(item: Item) -> None: del sys.last_type del sys.last_value del sys.last_traceback + if sys.version_info >= (3, 12, 0): + del sys.last_exc # type: ignore[attr-defined] except AttributeError: pass try: @@ -170,6 +175,8 @@ def pytest_runtest_call(item: Item) -> None: # Store trace info to allow postmortem debugging sys.last_type = type(e) sys.last_value = e + if sys.version_info >= (3, 12, 0): + sys.last_exc = e # type: ignore[attr-defined] assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next @@ -183,7 +190,7 @@ def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: def _update_current_test_var( - item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] + item: Item, when: Optional[Literal["setup", "call", "teardown"]] ) -> None: """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. @@ -216,15 +223,28 @@ def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str def call_and_report( - item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds + item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds ) -> TestReport: - call = call_runtest_hook(item, when, **kwds) - hook = item.ihook - report: TestReport = hook.pytest_runtest_makereport(item=item, call=call) + ihook = item.ihook + if when == "setup": + runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup + elif when == "call": + runtest_hook = ihook.pytest_runtest_call + elif when == "teardown": + runtest_hook = ihook.pytest_runtest_teardown + else: + assert False, f"Unhandled runtest hook case: {when}" + reraise: Tuple[Type[BaseException], ...] = (Exit,) + if not item.config.getoption("usepdb", False): + reraise += (KeyboardInterrupt,) + call = CallInfo.from_call( + lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise + ) + report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call) if log: - hook.pytest_runtest_logreport(report=report) + ihook.pytest_runtest_logreport(report=report) if check_interactive_exception(call, report): - hook.pytest_exception_interact(node=item, call=call, report=report) + ihook.pytest_exception_interact(node=item, call=call, report=report) return report @@ -243,30 +263,11 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> return True -def call_runtest_hook( - item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds -) -> "CallInfo[None]": - if when == "setup": - ihook: Callable[..., None] = item.ihook.pytest_runtest_setup - elif when == "call": - ihook = item.ihook.pytest_runtest_call - elif when == "teardown": - ihook = item.ihook.pytest_runtest_teardown - else: - assert False, f"Unhandled runtest hook case: {when}" - reraise: Tuple[Type[BaseException], ...] = (Exit,) - if not item.config.getoption("usepdb", False): - reraise += (KeyboardInterrupt,) - return CallInfo.from_call( - lambda: ihook(item=item, **kwds), when=when, reraise=reraise - ) - - TResult = TypeVar("TResult", covariant=True) @final -@attr.s(repr=False, init=False, auto_attribs=True) +@dataclasses.dataclass class CallInfo(Generic[TResult]): """Result/Exception info of a function invocation.""" @@ -280,7 +281,7 @@ class CallInfo(Generic[TResult]): #: The call duration, in seconds. duration: float #: The context of invocation: "collect", "setup", "call" or "teardown". - when: "Literal['collect', 'setup', 'call', 'teardown']" + when: Literal["collect", "setup", "call", "teardown"] def __init__( self, @@ -289,7 +290,7 @@ class CallInfo(Generic[TResult]): start: float, stop: float, duration: float, - when: "Literal['collect', 'setup', 'call', 'teardown']", + when: Literal["collect", "setup", "call", "teardown"], *, _ispytest: bool = False, ) -> None: @@ -317,8 +318,8 @@ class CallInfo(Generic[TResult]): @classmethod def from_call( cls, - func: "Callable[[], TResult]", - when: "Literal['collect', 'setup', 'call', 'teardown']", + func: Callable[[], TResult], + when: Literal["collect", "setup", "call", "teardown"], reraise: Optional[ Union[Type[BaseException], Tuple[Type[BaseException], ...]] ] = None, @@ -368,7 +369,28 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: - call = CallInfo.from_call(lambda: list(collector.collect()), "collect") + def collect() -> List[Union[Item, Collector]]: + # Before collecting, if this is a Directory, load the conftests. + # If a conftest import fails to load, it is considered a collection + # error of the Directory collector. This is why it's done inside of the + # CallInfo wrapper. + # + # Note: initial conftests are loaded early, not here. + if isinstance(collector, Directory): + collector.config.pluginmanager._loadconftestmodules( + collector.path, + collector.config.getoption("importmode"), + rootpath=collector.config.rootpath, + consider_namespace_packages=collector.config.getini( + "consider_namespace_packages" + ), + ) + + return list(collector.collect()) + + call = CallInfo.from_call( + collect, "collect", reraise=(KeyboardInterrupt, SystemExit) + ) longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None if not call.excinfo: outcome: Literal["passed", "skipped", "failed"] = "passed" @@ -376,14 +398,8 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") if unittest is not None: - # Type ignored because unittest is loaded dynamically. - skip_exceptions.append(unittest.SkipTest) # type: ignore + skip_exceptions.append(unittest.SkipTest) if isinstance(call.excinfo.value, tuple(skip_exceptions)): - if unittest is not None and isinstance( - call.excinfo.value, unittest.SkipTest # type: ignore[attr-defined] - ): - warnings.warn(UNITTEST_SKIP_DURING_COLLECTION, stacklevel=2) - outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") assert isinstance(r_, ExceptionChainRepr), repr(r_) @@ -518,22 +534,29 @@ class SetupState: stack is torn down. """ needed_collectors = nextitem and nextitem.listchain() or [] - exc = None + exceptions: List[BaseException] = [] while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break node, (finalizers, _) = self.stack.popitem() + these_exceptions = [] while finalizers: fin = finalizers.pop() try: fin() except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc + these_exceptions.append(e) + + if len(these_exceptions) == 1: + exceptions.extend(these_exceptions) + elif these_exceptions: + msg = f"errors while tearing down {node!r}" + exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1])) + + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup("errors during test teardown", exceptions[::-1]) if nextitem is None: assert not self.stack diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/scope.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/scope.py index 7a746fb9fa9..2c6e23208f2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/scope.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/scope.py @@ -7,15 +7,14 @@ would cause circular references. Also this makes the module light to import, as it should. """ + from enum import Enum from functools import total_ordering +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from typing_extensions import Literal - _ScopeName = Literal["session", "package", "module", "class", "function"] +_ScopeName = Literal["session", "package", "module", "class", "function"] @total_ordering @@ -33,11 +32,11 @@ class Scope(Enum): """ # Scopes need to be listed from lower to higher. - Function: "_ScopeName" = "function" - Class: "_ScopeName" = "class" - Module: "_ScopeName" = "module" - Package: "_ScopeName" = "package" - Session: "_ScopeName" = "session" + Function: _ScopeName = "function" + Class: _ScopeName = "class" + Module: _ScopeName = "module" + Package: _ScopeName = "package" + Session: _ScopeName = "session" def next_lower(self) -> "Scope": """Return the next lower scope.""" @@ -60,7 +59,7 @@ class Scope(Enum): @classmethod def from_user( - cls, scope_name: "_ScopeName", descr: str, where: Optional[str] = None + cls, scope_name: _ScopeName, descr: str, where: Optional[str] = None ) -> "Scope": """ Given a scope name from the user, return the equivalent Scope enum. Should be used diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setuponly.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setuponly.py index 531131ce726..39ab28b466b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setuponly.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setuponly.py @@ -2,7 +2,6 @@ from typing import Generator from typing import Optional from typing import Union -import pytest from _pytest._io.saferepr import saferepr from _pytest.config import Config from _pytest.config import ExitCode @@ -10,6 +9,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef from _pytest.fixtures import SubRequest from _pytest.scope import Scope +import pytest def pytest_addoption(parser: Parser) -> None: @@ -18,47 +18,52 @@ def pytest_addoption(parser: Parser) -> None: "--setuponly", "--setup-only", action="store_true", - help="only setup fixtures, do not execute tests.", + help="Only setup fixtures, do not execute tests", ) group.addoption( "--setupshow", "--setup-show", action="store_true", - help="show setup of fixtures while executing tests.", + help="Show setup of fixtures while executing tests", ) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest -) -> Generator[None, None, None]: - yield - if request.config.option.setupshow: - if hasattr(request, "param"): - # Save the fixture parameter so ._show_fixture_action() can - # display it now and during the teardown (in .finish()). - if fixturedef.ids: - if callable(fixturedef.ids): - param = fixturedef.ids(request.param) +) -> Generator[None, object, object]: + try: + return (yield) + finally: + if request.config.option.setupshow: + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + param = fixturedef.ids(request.param) + else: + param = fixturedef.ids[request.param_index] else: - param = fixturedef.ids[request.param_index] - else: - param = request.param - fixturedef.cached_param = param # type: ignore[attr-defined] - _show_fixture_action(fixturedef, "SETUP") + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] + _show_fixture_action(fixturedef, request.config, "SETUP") -def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: +def pytest_fixture_post_finalizer( + fixturedef: FixtureDef[object], request: SubRequest +) -> None: if fixturedef.cached_result is not None: - config = fixturedef._fixturemanager.config + config = request.config if config.option.setupshow: - _show_fixture_action(fixturedef, "TEARDOWN") + _show_fixture_action(fixturedef, request.config, "TEARDOWN") if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param # type: ignore[attr-defined] + del fixturedef.cached_param -def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: - config = fixturedef._fixturemanager.config +def _show_fixture_action( + fixturedef: FixtureDef[object], config: Config, msg: str +) -> None: capman = config.pluginmanager.getplugin("capturemanager") if capman: capman.suspend_global_capture() @@ -69,7 +74,7 @@ def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: scope_indent = list(reversed(Scope)).index(fixturedef._scope) tw.write(" " * 2 * scope_indent) tw.write( - "{step} {scope} {fixture}".format( + "{step} {scope} {fixture}".format( # noqa: UP032 (Readability) step=msg.ljust(8), # align the output to TEARDOWN scope=fixturedef.scope[0].upper(), fixture=fixturedef.argname, @@ -82,7 +87,7 @@ def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: tw.write(" (fixtures used: {})".format(", ".join(deps))) if hasattr(fixturedef, "cached_param"): - tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined] + tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") tw.flush() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setupplan.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setupplan.py index 9ba81ccaf0a..13c0df84ea1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setupplan.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/setupplan.py @@ -1,12 +1,12 @@ from typing import Optional from typing import Union -import pytest from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef from _pytest.fixtures import SubRequest +import pytest def pytest_addoption(parser: Parser) -> None: @@ -15,8 +15,8 @@ def pytest_addoption(parser: Parser) -> None: "--setupplan", "--setup-plan", action="store_true", - help="show what fixtures and tests would be executed but " - "don't execute anything.", + help="Show what fixtures and tests would be executed but " + "don't execute anything", ) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/skipping.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/skipping.py index ac7216f8385..188dcae3f1c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/skipping.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/skipping.py @@ -1,16 +1,17 @@ +# mypy: allow-untyped-defs """Support for skip/xfail functions and markers.""" + +from collections.abc import Mapping +import dataclasses import os import platform import sys import traceback -from collections.abc import Mapping from typing import Generator from typing import Optional from typing import Tuple from typing import Type -import attr - from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -20,6 +21,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.reports import BaseReport +from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.stash import StashKey @@ -31,12 +33,12 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", dest="runxfail", default=False, - help="report the results of xfail tests as if they were not marked", + help="Report the results of xfail tests as if they were not marked", ) parser.addini( "xfail_strict", - "default for the strict parameter of xfail " + "Default for the strict parameter of xfail " "markers when not given explicitly (default: False)", default=False, type="bool", @@ -104,13 +106,11 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, ): if not isinstance(dictionary, Mapping): raise ValueError( - "pytest_markeval_namespace() needs to return a dict, got {!r}".format( - dictionary - ) + f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}" ) globals_.update(dictionary) if hasattr(item, "obj"): - globals_.update(item.obj.__globals__) # type: ignore[attr-defined] + globals_.update(item.obj.__globals__) try: filename = f"<{mark.name} condition>" condition_code = compile(condition, filename, "eval") @@ -157,7 +157,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, return result, reason -@attr.s(slots=True, frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Skip: """The result of evaluate_skip_marks().""" @@ -192,10 +192,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]: return None -@attr.s(slots=True, frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Xfail: """The result of evaluate_xfail_marks().""" + __slots__ = ("reason", "run", "strict", "raises") + reason: str run: bool strict: bool @@ -242,7 +244,7 @@ def pytest_runtest_setup(item: Item) -> None: xfail("[NOTRUN] " + xfailed.reason) -@hookimpl(hookwrapper=True) +@hookimpl(wrapper=True) def pytest_runtest_call(item: Item) -> Generator[None, None, None]: xfailed = item.stash.get(xfailed_key, None) if xfailed is None: @@ -251,18 +253,20 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: if xfailed and not item.config.option.runxfail and not xfailed.run: xfail("[NOTRUN] " + xfailed.reason) - yield - - # The test run may have added an xfail mark dynamically. - xfailed = item.stash.get(xfailed_key, None) - if xfailed is None: - item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + try: + return (yield) + finally: + # The test run may have added an xfail mark dynamically. + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) -@hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item: Item, call: CallInfo[None]): - outcome = yield - rep = outcome.get_result() +@hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: Item, call: CallInfo[None] +) -> Generator[None, TestReport, TestReport]: + rep = yield xfailed = item.stash.get(xfailed_key, None) if item.config.option.runxfail: pass # don't interfere @@ -285,6 +289,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): else: rep.outcome = "passed" rep.wasxfail = xfailed.reason + return rep def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stash.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stash.py index e61d75b95f7..a4b829fc6dd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stash.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stash.py @@ -19,6 +19,8 @@ class StashKey(Generic[T]): A ``StashKey`` is associated with the type ``T`` of the value of the key. A ``StashKey`` is unique and cannot conflict with another key. + + .. versionadded:: 7.0 """ __slots__ = () @@ -61,6 +63,8 @@ class Stash: some_str = stash[some_str_key] # The static type of some_bool is bool. some_bool = stash[some_bool_key] + + .. versionadded:: 7.0 """ __slots__ = ("_storage",) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stepwise.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stepwise.py index 4d95a96b872..92d3a297e0d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stepwise.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/stepwise.py @@ -2,12 +2,13 @@ from typing import List from typing import Optional from typing import TYPE_CHECKING -import pytest from _pytest import nodes from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.reports import TestReport +import pytest + if TYPE_CHECKING: from _pytest.cacheprovider import Cache @@ -23,7 +24,7 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", default=False, dest="stepwise", - help="exit on test failure and continue from last failing test next time", + help="Exit on test failure and continue from last failing test next time", ) group.addoption( "--sw-skip", @@ -31,15 +32,15 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", default=False, dest="stepwise_skip", - help="ignore the first failing test but stop on the next failing test.\n" - "implicitly enables --stepwise.", + help="Ignore the first failing test but stop on the next failing test. " + "Implicitly enables --stepwise.", ) @pytest.hookimpl def pytest_configure(config: Config) -> None: if config.option.stepwise_skip: - # allow --stepwise-skip to work on it's own merits. + # allow --stepwise-skip to work on its own merits. config.option.stepwise = True if config.getoption("stepwise"): config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") @@ -48,6 +49,10 @@ def pytest_configure(config: Config) -> None: def pytest_sessionfinish(session: Session) -> None: if not session.config.getoption("stepwise"): assert session.config.cache is not None + if hasattr(session.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return # Clear the list of failing tests if the plugin is not active. session.config.cache.set(STEPWISE_CACHE_DIR, []) @@ -119,4 +124,8 @@ class StepwisePlugin: return None def pytest_sessionfinish(self) -> None: + if hasattr(self.config, "workerinput"): + # Do not update cache if this process is a xdist worker to prevent + # race conditions (#10641). + return self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/terminal.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/terminal.py index ccbd84d7d71..724d5c54d2f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/terminal.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/terminal.py @@ -1,24 +1,29 @@ +# mypy: allow-untyped-defs """Terminal reporting of the full testing process. This is a good source for looking at the various reporting hooks. """ + import argparse +from collections import Counter +import dataclasses import datetime +from functools import partial import inspect +from pathlib import Path import platform import sys -import warnings -from collections import Counter -from functools import partial -from pathlib import Path +import textwrap from typing import Any from typing import Callable -from typing import cast from typing import ClassVar from typing import Dict +from typing import final from typing import Generator from typing import List +from typing import Literal from typing import Mapping +from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Set @@ -26,17 +31,18 @@ from typing import TextIO from typing import Tuple from typing import TYPE_CHECKING from typing import Union +import warnings -import attr import pluggy -import _pytest._version from _pytest import nodes from _pytest import timing from _pytest._code import ExceptionInfo from _pytest._code.code import ExceptionRepr +from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth -from _pytest.compat import final +import _pytest._version +from _pytest.assertion.util import running_on_ci from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -50,9 +56,8 @@ from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport -if TYPE_CHECKING: - from typing_extensions import Literal +if TYPE_CHECKING: from _pytest.main import Session @@ -109,29 +114,49 @@ class MoreQuietAction(argparse.Action): namespace.quiet = getattr(namespace, "quiet", 0) + 1 +class TestShortLogReport(NamedTuple): + """Used to store the test status result category, shortletter and verbose word. + For example ``"rerun", "R", ("RERUN", {"yellow": True})``. + + :ivar category: + The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string. + + :ivar letter: + The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string. + + :ivar word: + Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``, + ``"ERROR"``, or the empty string. + """ + + category: str + letter: str + word: Union[str, Tuple[str, Mapping[str, bool]]] + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting", "reporting", after="general") + group = parser.getgroup("terminal reporting", "Reporting", after="general") group._addoption( "-v", "--verbose", action="count", default=0, dest="verbose", - help="increase verbosity.", + help="Increase verbosity", ) group._addoption( "--no-header", action="store_true", default=False, dest="no_header", - help="disable header", + help="Disable header", ) group._addoption( "--no-summary", action="store_true", default=False, dest="no_summary", - help="disable summary", + help="Disable summary", ) group._addoption( "-q", @@ -139,14 +164,14 @@ def pytest_addoption(parser: Parser) -> None: action=MoreQuietAction, default=0, dest="verbose", - help="decrease verbosity.", + help="Decrease verbosity", ) group._addoption( "--verbosity", dest="verbose", type=int, default=0, - help="set verbosity. Default is 0.", + help="Set verbosity. Default: 0.", ) group._addoption( "-r", @@ -154,7 +179,7 @@ def pytest_addoption(parser: Parser) -> None: dest="reportchars", default=_REPORTCHARS_DEFAULT, metavar="chars", - help="show extra test summary info as specified by chars: (f)ailed, " + help="Show extra test summary info as specified by chars: (f)ailed, " "(E)rror, (s)kipped, (x)failed, (X)passed, " "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " "(w)arnings are enabled by default (see --disable-warnings), " @@ -166,7 +191,7 @@ def pytest_addoption(parser: Parser) -> None: default=False, dest="disable_warnings", action="store_true", - help="disable warnings summary", + help="Disable warnings summary", ) group._addoption( "-l", @@ -174,7 +199,13 @@ def pytest_addoption(parser: Parser) -> None: action="store_true", dest="showlocals", default=False, - help="show locals in tracebacks (disabled by default).", + help="Show locals in tracebacks (disabled by default)", + ) + group._addoption( + "--no-showlocals", + action="store_false", + dest="showlocals", + help="Hide locals in tracebacks (negate --showlocals passed through addopts)", ) group._addoption( "--tb", @@ -183,7 +214,7 @@ def pytest_addoption(parser: Parser) -> None: dest="tbstyle", default="auto", choices=["auto", "long", "short", "no", "line", "native"], - help="traceback print mode (auto/long/short/line/native/no).", + help="Traceback print mode (auto/long/short/line/native/no)", ) group._addoption( "--show-capture", @@ -192,14 +223,14 @@ def pytest_addoption(parser: Parser) -> None: choices=["no", "stdout", "stderr", "log", "all"], default="all", help="Controls how captured stdout/stderr/log is shown on failed tests. " - "Default is 'all'.", + "Default: all.", ) group._addoption( "--fulltrace", "--full-trace", action="store_true", default=False, - help="don't cut any tracebacks (default is to cut).", + help="Don't cut any tracebacks (default is to cut)", ) group._addoption( "--color", @@ -208,20 +239,31 @@ def pytest_addoption(parser: Parser) -> None: dest="color", default="auto", choices=["yes", "no", "auto"], - help="color terminal output (yes/no/auto).", + help="Color terminal output (yes/no/auto)", ) group._addoption( "--code-highlight", default="yes", choices=["yes", "no"], - help="Whether code should be highlighted (only if --color is also enabled)", + help="Whether code should be highlighted (only if --color is also enabled). " + "Default: yes.", ) parser.addini( "console_output_style", - help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").', + help='Console output: "classic", or with additional progress information ' + '("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces ' + "progress even when capture=no)", default="progress", ) + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_TEST_CASES, + help=( + "Specify a verbosity level for test case execution, overriding the main level. " + "Higher levels will provide more detailed information about each test case executed." + ), + ) def pytest_configure(config: Config) -> None: @@ -277,7 +319,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: return outcome, letter, outcome.upper() -@attr.s(auto_attribs=True) +@dataclasses.dataclass class WarningReport: """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. @@ -334,16 +376,21 @@ class TerminalReporter: self._already_displayed_warnings: Optional[int] = None self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None - def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": + def _determine_show_progress_info(self) -> Literal["progress", "count", False]: """Return whether we should display progress information based on the current config.""" - # do not show progress if we are not capturing output (#3038) - if self.config.getoption("capture", "no") == "no": + # do not show progress if we are not capturing output (#3038) unless explicitly + # overridden by progress-even-when-capture-no + if ( + self.config.getoption("capture", "no") == "no" + and self.config.getini("console_output_style") + != "progress-even-when-capture-no" + ): return False # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow", False): return False cfg: str = self.config.getini("console_output_style") - if cfg == "progress": + if cfg in {"progress", "progress-even-when-capture-no"}: return "progress" elif cfg == "count": return "count" @@ -370,7 +417,7 @@ class TerminalReporter: @property def showfspath(self) -> bool: if self._showfspath is None: - return self.verbosity >= 0 + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) >= 0 return self._showfspath @showfspath.setter @@ -379,7 +426,7 @@ class TerminalReporter: @property def showlongtestinfo(self) -> bool: - return self.verbosity > 0 + return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) > 0 def hasopt(self, char: str) -> bool: char = {"xfailed": "x", "skipped": "s"}.get(char, char) @@ -410,6 +457,28 @@ class TerminalReporter: self._tw.line() self.currentfspath = None + def wrap_write( + self, + content: str, + *, + flush: bool = False, + margin: int = 8, + line_sep: str = "\n", + **markup: bool, + ) -> None: + """Wrap message with margin for progress info.""" + width_of_current_line = self._tw.width_of_current_line + wrapped = line_sep.join( + textwrap.wrap( + " " * width_of_current_line + content, + width=self._screen_width - margin, + drop_whitespace=True, + replace_whitespace=False, + ), + ) + wrapped = wrapped[width_of_current_line:] + self._tw.write(wrapped, flush=flush, **markup) + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: self._tw.write(content, flush=flush, **markup) @@ -509,10 +578,11 @@ class TerminalReporter: def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True rep = report - res: Tuple[ - str, str, Union[str, Tuple[str, Mapping[str, bool]]] - ] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) - category, letter, word = res + + res = TestShortLogReport( + *self.config.hook.pytest_report_teststatus(report=rep, config=self.config) + ) + category, letter, word = res.category, res.letter, res.word if not isinstance(word, tuple): markup = None else: @@ -534,7 +604,7 @@ class TerminalReporter: markup = {"yellow": True} else: markup = {} - if self.verbosity <= 0: + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0: self._tw.write(letter, **markup) else: self._progress_nodeids_reported.add(rep.nodeid) @@ -542,15 +612,21 @@ class TerminalReporter: if not running_xdist: self.write_ensure_prefix(line, word, **markup) if rep.skipped or hasattr(report, "wasxfail"): - available_width = ( - (self._tw.fullwidth - self._tw.width_of_current_line) - - len(" [100%]") - - 1 - ) reason = _get_raw_skip_reason(rep) - reason_ = _format_trimmed(" ({})", reason, available_width) - if reason and reason_ is not None: - self._tw.write(reason_) + if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) < 2: + available_width = ( + (self._tw.fullwidth - self._tw.width_of_current_line) + - len(" [100%]") + - 1 + ) + formatted_reason = _format_trimmed( + " ({})", reason, available_width + ) + else: + formatted_reason = f" ({reason})" + + if reason and formatted_reason is not None: + self.wrap_write(formatted_reason) if self._show_progress_info: self._write_progress_information_filling_space() else: @@ -574,7 +650,10 @@ class TerminalReporter: def pytest_runtest_logfinish(self, nodeid: str) -> None: assert self._session - if self.verbosity <= 0 and self._show_progress_info: + if ( + self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0 + and self._show_progress_info + ): if self._show_progress_info == "count": num_tests = self._session.testscollected progress_length = len(f" [{num_tests}/{num_tests}]") @@ -605,8 +684,8 @@ class TerminalReporter: return f" [ {collected} / {collected} ]" else: if collected: - return " [{:3d}%]".format( - len(self._progress_nodeids_reported) * 100 // collected + return ( + f" [{len(self._progress_nodeids_reported) * 100 // collected:3d}%]" ) return " [100%]" @@ -657,7 +736,7 @@ class TerminalReporter: errors = len(self.stats.get("error", [])) skipped = len(self.stats.get("skipped", [])) deselected = len(self.stats.get("deselected", [])) - selected = self._numcollected - errors - skipped - deselected + selected = self._numcollected - deselected line = "collected " if final else "collecting " line += ( str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") @@ -668,7 +747,7 @@ class TerminalReporter: line += " / %d deselected" % deselected if skipped: line += " / %d skipped" % skipped - if self._numcollected > selected > 0: + if self._numcollected > selected: line += " / %d selected" % selected if self.isatty: self.rewrite(line, bold=True, erase=True) @@ -691,9 +770,7 @@ class TerminalReporter: if pypy_version_info: verinfo = ".".join(map(str, pypy_version_info[:3])) msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]" - msg += ", pytest-{}, pluggy-{}".format( - _pytest._version.version, pluggy.__version__ - ) + msg += f", pytest-{_pytest._version.version}, pluggy-{pluggy.__version__}" if ( self.verbosity > 0 or self.config.option.debug @@ -717,16 +794,14 @@ class TerminalReporter: self.write_line(line) def pytest_report_header(self, config: Config) -> List[str]: - line = "rootdir: %s" % config.rootpath + result = [f"rootdir: {config.rootpath}"] if config.inipath: - line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) - - testpaths: List[str] = config.getini("testpaths") - if config.invocation_params.dir == config.rootpath and config.args == testpaths: - line += ", testpaths: {}".format(", ".join(testpaths)) + result.append("configfile: " + bestrelpath(config.rootpath, config.inipath)) - result = [line] + if config.args_source == Config.ArgsSource.TESTPATHS: + testpaths: List[str] = config.getini("testpaths") + result.append("testpaths: {}".format(", ".join(testpaths))) plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: @@ -756,8 +831,9 @@ class TerminalReporter: rep.toterminal(self._tw) def _printcollecteditems(self, items: Sequence[Item]) -> None: - if self.config.option.verbose < 0: - if self.config.option.verbose < -1: + test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) + if test_cases_verbosity < 0: + if test_cases_verbosity < -1: counts = Counter(item.nodeid.split("::", 1)[0] for item in items) for name, count in sorted(counts.items()): self._tw.line("%s: %d" % (name, count)) @@ -777,19 +853,18 @@ class TerminalReporter: stack.append(col) indent = (len(stack) - 1) * " " self._tw.line(f"{indent}{col}") - if self.config.option.verbose >= 1: + if test_cases_verbosity >= 1: obj = getattr(col, "obj", None) doc = inspect.getdoc(obj) if obj else None if doc: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] - ): - outcome = yield - outcome.get_result() + ) -> Generator[None, None, None]: + result = yield self._tw.line("") summary_exit_codes = ( ExitCode.OK, @@ -810,17 +885,22 @@ class TerminalReporter: elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() + return result - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() + self.summary_xfailures() self.summary_warnings() self.summary_passes() - yield - self.short_test_summary() - # Display any extra warnings from teardown here (if any). - self.summary_warnings() + self.summary_xpasses() + try: + return (yield) + finally: + self.short_test_summary() + # Display any extra warnings from teardown here (if any). + self.summary_warnings() def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) @@ -944,12 +1024,20 @@ class TerminalReporter: ) def summary_passes(self) -> None: + self.summary_passes_combined("passed", "PASSES", "P") + + def summary_xpasses(self) -> None: + self.summary_passes_combined("xpassed", "XPASSES", "X") + + def summary_passes_combined( + self, which_reports: str, sep_title: str, needed_opt: str + ) -> None: if self.config.option.tbstyle != "no": - if self.hasopt("P"): - reports: List[TestReport] = self.getreports("passed") + if self.hasopt(needed_opt): + reports: List[TestReport] = self.getreports(which_reports) if not reports: return - self.write_sep("=", "PASSES") + self.write_sep("=", sep_title) for rep in reports: if rep.sections: msg = self._getfailureheadline(rep) @@ -983,21 +1071,30 @@ class TerminalReporter: self._tw.line(content) def summary_failures(self) -> None: + self.summary_failures_combined("failed", "FAILURES") + + def summary_xfailures(self) -> None: + self.summary_failures_combined("xfailed", "XFAILURES", "x") + + def summary_failures_combined( + self, which_reports: str, sep_title: str, needed_opt: Optional[str] = None + ) -> None: if self.config.option.tbstyle != "no": - reports: List[BaseReport] = self.getreports("failed") - if not reports: - return - self.write_sep("=", "FAILURES") - if self.config.option.tbstyle == "line": - for rep in reports: - line = self._getcrashline(rep) - self.write_line(line) - else: - for rep in reports: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) - self._handle_teardown_sections(rep.nodeid) + if not needed_opt or self.hasopt(needed_opt): + reports: List[BaseReport] = self.getreports(which_reports) + if not reports: + return + self.write_sep("=", sep_title) + if self.config.option.tbstyle == "line": + for rep in reports: + line = self._getcrashline(rep) + self.write_line(line) + else: + for rep in reports: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) def summary_errors(self) -> None: if self.config.option.tbstyle != "no": @@ -1068,33 +1165,46 @@ class TerminalReporter: if not self.reportchars: return - def show_simple(stat, lines: List[str]) -> None: + def show_simple(lines: List[str], *, stat: str) -> None: failed = self.stats.get(stat, []) if not failed: return - termwidth = self._tw.fullwidth config = self.config for rep in failed: - line = _get_line_with_reprcrash_message(config, rep, termwidth) + color = _color_for_type.get(stat, _color_for_type_default) + line = _get_line_with_reprcrash_message( + config, rep, self._tw, {color: True} + ) lines.append(line) def show_xfailed(lines: List[str]) -> None: xfailed = self.stats.get("xfailed", []) for rep in xfailed: verbose_word = rep._get_verbose_word(self.config) - pos = _get_pos(self.config, rep) - lines.append(f"{verbose_word} {pos}") + markup_word = self._tw.markup( + verbose_word, **{_color_for_type["warnings"]: True} + ) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" reason = rep.wasxfail if reason: - lines.append(" " + str(reason)) + line += " - " + str(reason) + + lines.append(line) def show_xpassed(lines: List[str]) -> None: xpassed = self.stats.get("xpassed", []) for rep in xpassed: verbose_word = rep._get_verbose_word(self.config) - pos = _get_pos(self.config, rep) + markup_word = self._tw.markup( + verbose_word, **{_color_for_type["warnings"]: True} + ) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" reason = rep.wasxfail - lines.append(f"{verbose_word} {pos} {reason}") + if reason: + line += " - " + str(reason) + lines.append(line) def show_skipped(lines: List[str]) -> None: skipped: List[CollectReport] = self.stats.get("skipped", []) @@ -1102,24 +1212,27 @@ class TerminalReporter: if not fskips: return verbose_word = skipped[0]._get_verbose_word(self.config) + markup_word = self._tw.markup( + verbose_word, **{_color_for_type["warnings"]: True} + ) + prefix = "Skipped: " for num, fspath, lineno, reason in fskips: - if reason.startswith("Skipped: "): - reason = reason[9:] + if reason.startswith(prefix): + reason = reason[len(prefix) :] if lineno is not None: lines.append( - "%s [%d] %s:%d: %s" - % (verbose_word, num, fspath, lineno, reason) + "%s [%d] %s:%d: %s" % (markup_word, num, fspath, lineno, reason) ) else: - lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) + lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason)) REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = { "x": show_xfailed, "X": show_xpassed, - "f": partial(show_simple, "failed"), + "f": partial(show_simple, stat="failed"), "s": show_skipped, - "p": partial(show_simple, "passed"), - "E": partial(show_simple, "error"), + "p": partial(show_simple, stat="passed"), + "E": partial(show_simple, stat="error"), } lines: List[str] = [] @@ -1129,7 +1242,7 @@ class TerminalReporter: action(lines) if lines: - self.write_sep("=", "short test summary info") + self.write_sep("=", "short test summary info", cyan=True, bold=True) for line in lines: self.write_line(line) @@ -1154,7 +1267,7 @@ class TerminalReporter: def _set_main_color(self) -> None: unknown_types: List[str] = [] - for found_type in self.stats.keys(): + for found_type in self.stats: if found_type: # setup/teardown reports have an empty key, ignore them if found_type not in KNOWN_TYPES and found_type not in unknown_types: unknown_types.append(found_type) @@ -1243,9 +1356,14 @@ class TerminalReporter: return parts, main_color -def _get_pos(config: Config, rep: BaseReport): +def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport): nodeid = config.cwd_relative_nodeid(rep.nodeid) - return nodeid + path, *parts = nodeid.split("::") + if parts: + parts_markup = tw.markup("::".join(parts), bold=True) + return path + "::" + parts_markup + else: + return path def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]: @@ -1274,13 +1392,14 @@ def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str def _get_line_with_reprcrash_message( - config: Config, rep: BaseReport, termwidth: int + config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: Dict[str, bool] ) -> str: """Get summary line for a report, trying to add reprcrash message.""" verbose_word = rep._get_verbose_word(config) - pos = _get_pos(config, rep) + word = tw.markup(verbose_word, **word_markup) + node = _get_node_id_with_markup(tw, config, rep) - line = f"{verbose_word} {pos}" + line = f"{word} {node}" line_width = wcswidth(line) try: @@ -1289,8 +1408,11 @@ def _get_line_with_reprcrash_message( except AttributeError: pass else: - available_width = termwidth - line_width - msg = _format_trimmed(" - {}", msg, available_width) + if running_on_ci() or config.option.verbose >= 2: + msg = f" - {msg}" + else: + available_width = tw.fullwidth - line_width + msg = _format_trimmed(" - {}", msg, available_width) if msg is not None: line += msg @@ -1354,7 +1476,7 @@ def _plugin_nameversions(plugininfo) -> List[str]: values: List[str] = [] for plugin, dist in plugininfo: # Gets us name and version! - name = "{dist.project_name}-{dist.version}".format(dist=dist) + name = f"{dist.project_name}-{dist.version}" # Questionable convenience, but it keeps things short. if name.startswith("pytest-"): name = name[7:] @@ -1379,7 +1501,7 @@ def _get_raw_skip_reason(report: TestReport) -> str: The string is just the part given by the user. """ if hasattr(report, "wasxfail"): - reason = cast(str, report.wasxfail) + reason = report.wasxfail if reason.startswith("reason: "): reason = reason[len("reason: ") :] return reason diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/threadexception.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/threadexception.py index 43341e739a0..09faf661b91 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/threadexception.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/threadexception.py @@ -1,12 +1,12 @@ import threading import traceback -import warnings from types import TracebackType from typing import Any from typing import Callable from typing import Generator from typing import Optional from typing import Type +import warnings import pytest @@ -59,30 +59,34 @@ class catch_threading_exception: def thread_exception_runtest_hook() -> Generator[None, None, None]: with catch_threading_exception() as cm: - yield - if cm.args: - thread_name = "<unknown>" if cm.args.thread is None else cm.args.thread.name - msg = f"Exception in thread {thread_name}\n\n" - msg += "".join( - traceback.format_exception( - cm.args.exc_type, - cm.args.exc_value, - cm.args.exc_traceback, + try: + yield + finally: + if cm.args: + thread_name = ( + "<unknown>" if cm.args.thread is None else cm.args.thread.name + ) + msg = f"Exception in thread {thread_name}\n\n" + msg += "".join( + traceback.format_exception( + cm.args.exc_type, + cm.args.exc_value, + cm.args.exc_traceback, + ) ) - ) - warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, trylast=True) +@pytest.hookimpl(wrapper=True, trylast=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/timing.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/timing.py index 925163a5858..0541dc8e0a1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/timing.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/timing.py @@ -5,8 +5,10 @@ pytest runtime information (issue #185). Fixture "mock_timing" also interacts with this module for pytest's own tests. """ + from time import perf_counter from time import sleep from time import time + __all__ = ["perf_counter", "sleep", "time"] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/tmpdir.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/tmpdir.py index f901fd5727c..72efed3e87a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/tmpdir.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/tmpdir.py @@ -1,40 +1,63 @@ +# mypy: allow-untyped-defs """Support for providing temporary directories to test functions.""" + +import dataclasses import os +from pathlib import Path import re -import sys +from shutil import rmtree import tempfile -from pathlib import Path +from typing import Any +from typing import Dict +from typing import final +from typing import Generator +from typing import Literal from typing import Optional +from typing import Union -import attr - +from .pathlib import cleanup_dead_symlinks from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf -from _pytest.compat import final +from _pytest.compat import get_user_id from _pytest.config import Config +from _pytest.config import ExitCode +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import TestReport +from _pytest.stash import StashKey + + +tmppath_result_key = StashKey[Dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] @final -@attr.s(init=False) +@dataclasses.dataclass class TempPathFactory: """Factory for temporary directories under the common base temp directory. The base directory can be configured using the ``--basetemp`` option. """ - _given_basetemp = attr.ib(type=Optional[Path]) - _trace = attr.ib() - _basetemp = attr.ib(type=Optional[Path]) + _given_basetemp: Optional[Path] + # pluggy TagTracerSub, not currently exposed, so Any. + _trace: Any + _basetemp: Optional[Path] + _retention_count: int + _retention_policy: RetentionType def __init__( self, given_basetemp: Optional[Path], + retention_count: int, + retention_policy: RetentionType, trace, basetemp: Optional[Path] = None, *, @@ -49,6 +72,8 @@ class TempPathFactory: # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). self._given_basetemp = Path(os.path.abspath(str(given_basetemp))) self._trace = trace + self._retention_count = retention_count + self._retention_policy = retention_policy self._basetemp = basetemp @classmethod @@ -63,9 +88,23 @@ class TempPathFactory: :meta private: """ check_ispytest(_ispytest) + count = int(config.getini("tmp_path_retention_count")) + if count < 0: + raise ValueError( + f"tmp_path_retention_count must be >= 0. Current input: {count}." + ) + + policy = config.getini("tmp_path_retention_policy") + if policy not in ("all", "failed", "none"): + raise ValueError( + f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}." + ) + return cls( given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir"), + retention_count=count, + retention_policy=policy, _ispytest=True, ) @@ -100,7 +139,11 @@ class TempPathFactory: return p def getbasetemp(self) -> Path: - """Return the base temporary directory, creating it if needed.""" + """Return the base temporary directory, creating it if needed. + + :returns: + The base temporary directory. + """ if self._basetemp is not None: return self._basetemp @@ -129,23 +172,23 @@ class TempPathFactory: # Also, to keep things private, fixup any world-readable temp # rootdir's permissions. Historically 0o755 was used, so we can't # just error out on this, at least for a while. - if sys.platform != "win32": - uid = os.getuid() + uid = get_user_id() + if uid is not None: rootdir_stat = rootdir.stat() - # getuid shouldn't fail, but cpython defines such a case. - # Let's hope for the best. - if uid != -1: - if rootdir_stat.st_uid != uid: - raise OSError( - f"The temporary directory {rootdir} is not owned by the current user. " - "Fix this and try again." - ) - if (rootdir_stat.st_mode & 0o077) != 0: - os.chmod(rootdir, rootdir_stat.st_mode & ~0o077) + if rootdir_stat.st_uid != uid: + raise OSError( + f"The temporary directory {rootdir} is not owned by the current user. " + "Fix this and try again." + ) + if (rootdir_stat.st_mode & 0o077) != 0: + os.chmod(rootdir, rootdir_stat.st_mode & ~0o077) + keep = self._retention_count + if self._retention_policy == "none": + keep = 0 basetemp = make_numbered_dir_with_cleanup( prefix="pytest-", root=rootdir, - keep=3, + keep=keep, lock_timeout=LOCK_TIMEOUT, mode=0o700, ) @@ -158,11 +201,12 @@ class TempPathFactory: def get_user() -> Optional[str]: """Return the current user name, or None if getuser() does not work in the current environment (see #1010).""" - import getpass - try: + # In some exotic environments, getpass may not be importable. + import getpass + return getpass.getuser() - except (ImportError, KeyError): + except (ImportError, OSError, KeyError): return None @@ -179,6 +223,21 @@ def pytest_configure(config: Config) -> None: mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) +def pytest_addoption(parser: Parser) -> None: + parser.addini( + "tmp_path_retention_count", + help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.", + default=3, + ) + + parser.addini( + "tmp_path_retention_policy", + help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. " + "(all/failed/none)", + default="all", + ) + + @fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`pytest.TempPathFactory` instance for the test session.""" @@ -195,17 +254,69 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: @fixture -def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: +def tmp_path( + request: FixtureRequest, tmp_path_factory: TempPathFactory +) -> Generator[Path, None, None]: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + and old bases are removed after 3 sessions, to aid in debugging. + This behavior can be configured with :confval:`tmp_path_retention_count` and + :confval:`tmp_path_retention_policy`. + If ``--basetemp`` is used then it is cleared each session. See + :ref:`temporary directory location and retention`. The returned object is a :class:`pathlib.Path` object. """ + path = _mk_tmp(request, tmp_path_factory) + yield path + + # Remove the tmpdir if the policy is "failed" and the test passed. + tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore + policy = tmp_path_factory._retention_policy + result_dict = request.node.stash[tmppath_result_key] + + if policy == "failed" and result_dict.get("call", True): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(path, ignore_errors=True) + + del request.node.stash[tmppath_result_key] + + +def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]): + """After each session, remove base directory if all the tests passed, + the policy is "failed", and the basetemp is not specified by a user. + """ + tmp_path_factory: TempPathFactory = session.config._tmp_path_factory + basetemp = tmp_path_factory._basetemp + if basetemp is None: + return + + policy = tmp_path_factory._retention_policy + if ( + exitstatus == 0 + and policy == "failed" + and tmp_path_factory._given_basetemp is None + ): + if basetemp.is_dir(): + # We do a "best effort" to remove files, but it might not be possible due to some leaked resource, + # permissions, etc, in which case we ignore it. + rmtree(basetemp, ignore_errors=True) + + # Remove dead symlinks. + if basetemp.is_dir(): + cleanup_dead_symlinks(basetemp) + - return _mk_tmp(request, tmp_path_factory) +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport( + item: Item, call +) -> Generator[None, TestReport, TestReport]: + rep = yield + assert rep.when is not None + empty: Dict[str, bool] = {} + item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed + return rep diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unittest.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unittest.py index 0315168b044..8f1791bf744 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unittest.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unittest.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Discover and run std-library "unittest" style tests.""" + import sys import traceback import types @@ -14,8 +16,6 @@ from typing import TYPE_CHECKING from typing import Union import _pytest._code -import pytest -from _pytest.compat import getimfunc from _pytest.compat import is_async_function from _pytest.config import hookimpl from _pytest.fixtures import FixtureRequest @@ -27,12 +27,17 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function -from _pytest.python import PyCollector +from _pytest.python import Module from _pytest.runner import CallInfo -from _pytest.scope import Scope +import pytest + + +if sys.version_info[:2] < (3, 11): + from exceptiongroup import ExceptionGroup if TYPE_CHECKING: import unittest + import twisted.trial.unittest _SysExcInfoType = Union[ @@ -42,7 +47,7 @@ if TYPE_CHECKING: def pytest_pycollect_makeitem( - collector: PyCollector, name: str, obj: object + collector: Union[Module, Class], name: str, obj: object ) -> Optional["UnitTestCase"]: # Has unittest been imported and is obj a subclass of its TestCase? try: @@ -53,8 +58,7 @@ def pytest_pycollect_makeitem( except Exception: return None # Yes, so let's collect it. - item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) - return item + return UnitTestCase.from_parent(collector, name=name, obj=obj) class UnitTestCase(Class): @@ -62,6 +66,14 @@ class UnitTestCase(Class): # to declare that our children do not support funcargs. nofuncargs = True + def newinstance(self): + # TestCase __init__ takes the method (test) name. The TestCase + # constructor treats the name "runTest" as a special no-op, so it can be + # used when a dummy instance is needed. While unittest.TestCase has a + # default, some subclasses omit the default (#9610), so always supply + # it. + return self.obj("runTest") + def collect(self) -> Iterable[Union[Item, Collector]]: from unittest import TestLoader @@ -71,143 +83,139 @@ class UnitTestCase(Class): skipped = _is_skipped(cls) if not skipped: - self._inject_setup_teardown_fixtures(cls) - self._inject_setup_class_fixture() + self._register_unittest_setup_method_fixture(cls) + self._register_unittest_setup_class_fixture(cls) + self._register_setup_class_fixture() + + self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) - self.session._fixturemanager.parsefactories(self, unittest=True) loader = TestLoader() foundsomething = False for name in loader.getTestCaseNames(self.obj): x = getattr(self.obj, name) if not getattr(x, "__test__", True): continue - funcobj = getimfunc(x) - yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) + yield TestCaseFunction.from_parent(self, name=name) foundsomething = True if not foundsomething: runtest = getattr(self.obj, "runTest", None) if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) - # Type ignored because `ut` is an opaque module. - if ut is None or runtest != ut.TestCase.runTest: # type: ignore + if ut is None or runtest != ut.TestCase.runTest: yield TestCaseFunction.from_parent(self, name="runTest") - def _inject_setup_teardown_fixtures(self, cls: type) -> None: - """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding - teardown functions (#517).""" - class_fixture = _make_xunit_fixture( - cls, - "setUpClass", - "tearDownClass", - "doClassCleanups", - scope=Scope.Class, - pass_self=False, - ) - if class_fixture: - cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] - - method_fixture = _make_xunit_fixture( - cls, - "setup_method", - "teardown_method", - None, - scope=Scope.Function, - pass_self=True, - ) - if method_fixture: - cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] - - -def _make_xunit_fixture( - obj: type, - setup_name: str, - teardown_name: str, - cleanup_name: Optional[str], - scope: Scope, - pass_self: bool, -): - setup = getattr(obj, setup_name, None) - teardown = getattr(obj, teardown_name, None) - if setup is None and teardown is None: - return None - - if cleanup_name: - cleanup = getattr(obj, cleanup_name, lambda *args: None) - else: - - def cleanup(*args): - pass - - @pytest.fixture( - scope=scope.value, - autouse=True, - # Use a unique name to speed up lookup. - name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}", - ) - def fixture(self, request: FixtureRequest) -> Generator[None, None, None]: - if _is_skipped(self): - reason = self.__unittest_skip_why__ - raise pytest.skip.Exception(reason, _use_item_location=True) - if setup is not None: - try: - if pass_self: - setup(self, request.function) - else: + def _register_unittest_setup_class_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setUpClass and + tearDownClass (#517).""" + setup = getattr(cls, "setUpClass", None) + teardown = getattr(cls, "tearDownClass", None) + if setup is None and teardown is None: + return None + cleanup = getattr(cls, "doClassCleanups", lambda: None) + + def process_teardown_exceptions() -> None: + # tearDown_exceptions is a list set in the class containing exc_infos for errors during + # teardown for the class. + exc_infos = getattr(cls, "tearDown_exceptions", None) + if not exc_infos: + return + exceptions = [exc for (_, exc, _) in exc_infos] + # If a single exception, raise it directly as this provides a more readable + # error (hopefully this will improve in #12255). + if len(exceptions) == 1: + raise exceptions[0] + else: + raise ExceptionGroup("Unittest class cleanup errors", exceptions) + + def unittest_setup_class_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: + cls = request.cls + if _is_skipped(cls): + reason = cls.__unittest_skip_why__ + raise pytest.skip.Exception(reason, _use_item_location=True) + if setup is not None: + try: setup() - # unittest does not call the cleanup function for every BaseException, so we - # follow this here. - except Exception: - if pass_self: - cleanup(self) - else: + # unittest does not call the cleanup function for every BaseException, so we + # follow this here. + except Exception: cleanup() - - raise - yield - try: - if teardown is not None: - if pass_self: - teardown(self, request.function) - else: + process_teardown_exceptions() + raise + yield + try: + if teardown is not None: teardown() - finally: - if pass_self: - cleanup(self) - else: + finally: cleanup() + process_teardown_exceptions() + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setUpClass_fixture_{cls.__qualname__}", + func=unittest_setup_class_fixture, + nodeid=self.nodeid, + scope="class", + autouse=True, + ) - return fixture + def _register_unittest_setup_method_fixture(self, cls: type) -> None: + """Register an auto-use fixture to invoke setup_method and + teardown_method (#517).""" + setup = getattr(cls, "setup_method", None) + teardown = getattr(cls, "teardown_method", None) + if setup is None and teardown is None: + return None + + def unittest_setup_method_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: + self = request.instance + if _is_skipped(self): + reason = self.__unittest_skip_why__ + raise pytest.skip.Exception(reason, _use_item_location=True) + if setup is not None: + setup(self, request.function) + yield + if teardown is not None: + teardown(self, request.function) + + self.session._fixturemanager._register_fixture( + # Use a unique name to speed up lookup. + name=f"_unittest_setup_method_fixture_{cls.__qualname__}", + func=unittest_setup_method_fixture, + nodeid=self.nodeid, + scope="function", + autouse=True, + ) class TestCaseFunction(Function): nofuncargs = True _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None - _testcase: Optional["unittest.TestCase"] = None - def _getobj(self): - assert self.parent is not None - # Unlike a regular Function in a Class, where `item.obj` returns - # a *bound* method (attached to an instance), TestCaseFunction's - # `obj` returns an *unbound* method (not attached to an instance). - # This inconsistency is probably not desirable, but needs some - # consideration before changing. - return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] + def _getinstance(self): + assert isinstance(self.parent, UnitTestCase) + return self.parent.obj(self.name) + + # Backward compat for pytest-django; can be removed after pytest-django + # updates + some slack. + @property + def _testcase(self): + return self.instance def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). self._explicit_tearDown: Optional[Callable[[], None]] = None - assert self.parent is not None - self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] - self._obj = getattr(self._testcase, self.name) - if hasattr(self, "_request"): - self._request._fillfixtures() + super().setup() def teardown(self) -> None: + super().teardown() if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None - self._testcase = None self._obj = None def startTest(self, testcase: "unittest.TestCase") -> None: @@ -217,11 +225,13 @@ class TestCaseFunction(Function): # Unwrap potential exception info (see twisted trial support below). rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: - excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type] + excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info( + rawexcinfo # type: ignore[arg-type] + ) # Invoke the attributes to trigger storing the traceback # trial causes some issue there. - excinfo.value - excinfo.traceback + _ = excinfo.value + _ = excinfo.traceback except TypeError: try: try: @@ -237,7 +247,7 @@ class TestCaseFunction(Function): except BaseException: fail( "ERROR: Unknown Incompatible Exception " - "representation:\n%r" % (rawexcinfo,), + f"representation:\n{rawexcinfo!r}", pytrace=False, ) except KeyboardInterrupt: @@ -298,17 +308,20 @@ class TestCaseFunction(Function): def stopTest(self, testcase: "unittest.TestCase") -> None: pass + def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None: + pass + def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing - assert self._testcase is not None + testcase = self.instance + assert testcase is not None maybe_wrap_pytest_function_for_tracing(self) # Let the unittest framework handle async functions. if is_async_function(self.obj): - # Type ignored because self acts as the TestResult, but is not actually one. - self._testcase(result=self) # type: ignore[arg-type] + testcase(result=self) else: # When --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up @@ -316,27 +329,31 @@ class TestCaseFunction(Function): # Arguably we could always postpone tearDown(), but this changes the moment where the # TestCase instance interacts with the results object, so better to only do it # when absolutely needed. - if self.config.getoption("usepdb") and not _is_skipped(self.obj): - self._explicit_tearDown = self._testcase.tearDown - setattr(self._testcase, "tearDown", lambda *args: None) + # We need to consider if the test itself is skipped, or the whole class. + assert isinstance(self.parent, UnitTestCase) + skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) + if self.config.getoption("usepdb") and not skipped: + self._explicit_tearDown = testcase.tearDown + setattr(testcase, "tearDown", lambda *args: None) # We need to update the actual bound method with self.obj, because # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. - setattr(self._testcase, self.name, self.obj) + setattr(testcase, self.name, self.obj) try: - self._testcase(result=self) # type: ignore[arg-type] + testcase(result=self) finally: - delattr(self._testcase, self.name) + delattr(testcase, self.name) - def _prunetraceback( + def _traceback_filter( self, excinfo: _pytest._code.ExceptionInfo[BaseException] - ) -> None: - super()._prunetraceback(excinfo) - traceback = excinfo.traceback.filter( - lambda x: not x.frame.f_globals.get("__unittest") + ) -> _pytest._code.Traceback: + traceback = super()._traceback_filter(excinfo) + ntraceback = traceback.filter( + lambda x: not x.frame.f_globals.get("__unittest"), ) - if traceback: - excinfo.traceback = traceback + if not ntraceback: + ntraceback = traceback + return ntraceback @hookimpl(tryfirst=True) @@ -354,11 +371,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # its own nose.SkipTest. For unittest TestCases, SkipTest is already # handled internally, and doesn't reach here. unittest = sys.modules.get("unittest") - if ( - unittest - and call.excinfo - and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined] - ): + if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest): excinfo = call.excinfo call2 = CallInfo[None].from_call( lambda: pytest.skip(str(excinfo.value)), call.when @@ -367,14 +380,21 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # Twisted trial support. +classImplements_has_run = False -@hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: ut: Any = sys.modules["twisted.python.failure"] + global classImplements_has_run Failure__init__ = ut.Failure.__init__ - check_testcase_implements_trial_reporter() + if not classImplements_has_run: + from twisted.trial.itrial import IReporter + from zope.interface import classImplements + + classImplements(TestCaseFunction, IReporter) + classImplements_has_run = True def excstore( self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None @@ -393,20 +413,13 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: Failure__init__(self, exc_value, exc_type, exc_tb) ut.Failure.__init__ = excstore - yield - ut.Failure.__init__ = Failure__init__ + try: + res = yield + finally: + ut.Failure.__init__ = Failure__init__ else: - yield - - -def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: - if done: - return - from zope.interface import classImplements - from twisted.trial.itrial import IReporter - - classImplements(TestCaseFunction, IReporter) - done.append(1) + res = yield + return res def _is_skipped(obj) -> bool: diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py index fcb5d8237c1..f649267abf1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/unraisableexception.py @@ -1,12 +1,12 @@ import sys import traceback -import warnings from types import TracebackType from typing import Any from typing import Callable from typing import Generator from typing import Optional from typing import Type +import warnings import pytest @@ -61,33 +61,35 @@ class catch_unraisable_exception: def unraisable_exception_runtest_hook() -> Generator[None, None, None]: with catch_unraisable_exception() as cm: - yield - if cm.unraisable: - if cm.unraisable.err_msg is not None: - err_msg = cm.unraisable.err_msg - else: - err_msg = "Exception ignored in" - msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" - msg += "".join( - traceback.format_exception( - cm.unraisable.exc_type, - cm.unraisable.exc_value, - cm.unraisable.exc_traceback, + try: + yield + finally: + if cm.unraisable: + if cm.unraisable.err_msg is not None: + err_msg = cm.unraisable.err_msg + else: + err_msg = "Exception ignored in" + msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" + msg += "".join( + traceback.format_exception( + cm.unraisable.exc_type, + cm.unraisable.exc_value, + cm.unraisable.exc_traceback, + ) ) - ) - warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warning_types.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warning_types.py index 2a97a319789..a5884f29582 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warning_types.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warning_types.py @@ -1,11 +1,12 @@ +import dataclasses +import inspect +from types import FunctionType from typing import Any +from typing import final from typing import Generic from typing import Type from typing import TypeVar - -import attr - -from _pytest.compat import final +import warnings class PytestWarning(UserWarning): @@ -48,16 +49,14 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): __module__ = "pytest" -@final -class PytestRemovedIn7Warning(PytestDeprecationWarning): - """Warning class for features that will be removed in pytest 7.""" +class PytestRemovedIn9Warning(PytestDeprecationWarning): + """Warning class for features that will be removed in pytest 9.""" __module__ = "pytest" -@final -class PytestRemovedIn8Warning(PytestDeprecationWarning): - """Warning class for features that will be removed in pytest 8.""" +class PytestReturnNotNoneWarning(PytestWarning): + """Warning emitted when a test function is returning value other than None.""" __module__ = "pytest" @@ -74,15 +73,11 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): @classmethod def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": - return cls( - "{apiname} is an experimental api that may change over time".format( - apiname=apiname - ) - ) + return cls(f"{apiname} is an experimental api that may change over time") @final -class PytestUnhandledCoroutineWarning(PytestWarning): +class PytestUnhandledCoroutineWarning(PytestReturnNotNoneWarning): """Warning emitted for an unhandled coroutine. A coroutine was encountered when collecting test functions, but was not @@ -129,7 +124,7 @@ _W = TypeVar("_W", bound=PytestWarning) @final -@attr.s(auto_attribs=True) +@dataclasses.dataclass class UnformattedWarning(Generic[_W]): """A warning meant to be formatted during runtime. @@ -143,3 +138,28 @@ class UnformattedWarning(Generic[_W]): def format(self, **kwargs: Any) -> _W: """Return an instance of the warning category, formatted with given kwargs.""" return self.category(self.template.format(**kwargs)) + + +def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None: + """ + Issue the warning :param:`message` for the definition of the given :param:`method` + + this helps to log warnings for functions defined prior to finding an issue with them + (like hook wrappers being marked in a legacy mechanism) + """ + lineno = method.__code__.co_firstlineno + filename = inspect.getfile(method) + module = method.__module__ + mod_globals = method.__globals__ + try: + warnings.warn_explicit( + message, + type(message), + filename=filename, + module=module, + registry=mod_globals.setdefault("__warningregistry__", {}), + lineno=lineno, + ) + except Warning as w: + # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message. + raise type(w)(f"{w}\n at {filename}:{lineno}") from None diff --git a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warnings.py b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warnings.py index c0c946cbde5..22590892f8d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warnings.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/_pytest/warnings.py @@ -1,20 +1,18 @@ -import sys -import warnings +# mypy: allow-untyped-defs from contextlib import contextmanager +import sys from typing import Generator +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING +import warnings -import pytest from _pytest.config import apply_warning_filters from _pytest.config import Config from _pytest.config import parse_warning_filter from _pytest.main import Session from _pytest.nodes import Item from _pytest.terminal import TerminalReporter - -if TYPE_CHECKING: - from typing_extensions import Literal +import pytest def pytest_configure(config: Config) -> None: @@ -29,7 +27,7 @@ def pytest_configure(config: Config) -> None: def catch_warnings_for_item( config: Config, ihook, - when: "Literal['config', 'collect', 'runtest']", + when: Literal["config", "collect", "runtest"], item: Optional[Item], ) -> Generator[None, None, None]: """Context manager that catches warnings generated in the contained execution block. @@ -49,7 +47,8 @@ def catch_warnings_for_item( warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) - warnings.filterwarnings("error", category=pytest.PytestRemovedIn7Warning) + # To be enabled in pytest 9.0.0. + # warnings.filterwarnings("error", category=pytest.PytestRemovedIn9Warning) apply_warning_filters(config_filters, cmdline_filters) @@ -60,25 +59,18 @@ def catch_warnings_for_item( for arg in mark.args: warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) - yield - - for warning_message in log: - ihook.pytest_warning_captured.call_historic( - kwargs=dict( - warning_message=warning_message, - when=when, - item=item, - location=None, - ) - ) - ihook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=warning_message, - nodeid=nodeid, - when=when, - location=None, + try: + yield + finally: + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) ) - ) def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: @@ -91,27 +83,44 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: warning_message.lineno, warning_message.line, ) + if warning_message.source is not None: + try: + import tracemalloc + except ImportError: + pass + else: + tb = tracemalloc.get_object_traceback(warning_message.source) + if tb is not None: + formatted_tb = "\n".join(tb.format()) + # Use a leading new line to better separate the (large) output + # from the traceback to the previous warning text. + msg += f"\nObject allocated at:\n{formatted_tb}" + else: + # No need for a leading new line. + url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings" + msg += "Enable tracemalloc to get traceback where the object was allocated.\n" + msg += f"See {url} for more info." return msg -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: with catch_warnings_for_item( config=item.config, ihook=item.ihook, when="runtest", item=item ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection(session: Session) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection(session: Session) -> Generator[None, object, object]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="collect", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, ) -> Generator[None, None, None]: @@ -119,23 +128,23 @@ def pytest_terminal_summary( with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_load_initial_conftests( early_config: "Config", ) -> Generator[None, None, None]: with catch_warnings_for_item( config=early_config, ihook=early_config.hook, when="config", item=None ): - yield + return (yield) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/py.py b/tests/wpt/tests/tools/third_party/pytest/src/py.py new file mode 100644 index 00000000000..d1c39d203a8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/src/py.py @@ -0,0 +1,13 @@ +# shim for pylib going away +# if pylib is installed this file will get skipped +# (`py/__init__.py` has higher precedence) +import sys + +import _pytest._py.error as error +import _pytest._py.path as path + + +sys.modules["py.error"] = error +sys.modules["py.path"] = path + +__all__ = ["error", "path"] diff --git a/tests/wpt/tests/tools/third_party/pytest/src/pytest/__init__.py b/tests/wpt/tests/tools/third_party/pytest/src/pytest/__init__.py index 6050fd11248..c6b6de827e9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/pytest/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/pytest/__init__.py @@ -1,6 +1,6 @@ # PYTHON_ARGCOMPLETE_OK """pytest: unit and functional testing with Python.""" -from . import collect + from _pytest import __version__ from _pytest import version_tuple from _pytest._code import ExceptionInfo @@ -19,8 +19,9 @@ from _pytest.config import UsageError from _pytest.config.argparsing import OptionGroup from _pytest.config.argparsing import Parser from _pytest.debugging import pytestPDB as __pytestPDB -from _pytest.fixtures import _fillfuncargs +from _pytest.doctest import DoctestItem from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest from _pytest.fixtures import yield_fixture @@ -28,6 +29,7 @@ from _pytest.freeze_support import freeze_includes from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir from _pytest.logging import LogCaptureFixture +from _pytest.main import Dir from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark @@ -36,6 +38,7 @@ from _pytest.mark import MarkGenerator from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector +from _pytest.nodes import Directory from _pytest.nodes import File from _pytest.nodes import Item from _pytest.outcomes import exit @@ -63,6 +66,7 @@ from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.stash import Stash from _pytest.stash import StashKey +from _pytest.terminal import TestShortLogReport from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestCacheWarning @@ -70,38 +74,41 @@ from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning -from _pytest.warning_types import PytestRemovedIn7Warning -from _pytest.warning_types import PytestRemovedIn8Warning +from _pytest.warning_types import PytestRemovedIn9Warning +from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledThreadExceptionWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnraisableExceptionWarning from _pytest.warning_types import PytestWarning + set_trace = __pytestPDB.set_trace __all__ = [ "__version__", - "_fillfuncargs", "approx", "Cache", "CallInfo", "CaptureFixture", "Class", "cmdline", - "collect", "Collector", "CollectReport", "Config", "console_main", "deprecated_call", + "Dir", + "Directory", + "DoctestItem", "exit", "ExceptionInfo", "ExitCode", "fail", "File", "fixture", + "FixtureDef", "FixtureLookupError", "FixtureRequest", "freeze_includes", @@ -131,8 +138,8 @@ __all__ = [ "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", - "PytestRemovedIn7Warning", - "PytestRemovedIn8Warning", + "PytestRemovedIn9Warning", + "PytestReturnNotNoneWarning", "Pytester", "PytestPluginManager", "PytestUnhandledCoroutineWarning", @@ -154,18 +161,10 @@ __all__ = [ "TempPathFactory", "Testdir", "TestReport", + "TestShortLogReport", "UsageError", "WarningsRecorder", "warns", "xfail", "yield_fixture", ] - - -def __getattr__(name: str) -> object: - if name == "Instance": - # The import emits a deprecation warning. - from _pytest.python import Instance - - return Instance - raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/tests/wpt/tests/tools/third_party/pytest/src/pytest/__main__.py b/tests/wpt/tests/tools/third_party/pytest/src/pytest/__main__.py index b170152937b..e4cb67d5dd5 100644 --- a/tests/wpt/tests/tools/third_party/pytest/src/pytest/__main__.py +++ b/tests/wpt/tests/tools/third_party/pytest/src/pytest/__main__.py @@ -1,5 +1,7 @@ """The pytest entry point.""" + import pytest + if __name__ == "__main__": raise SystemExit(pytest.console_main()) diff --git a/tests/wpt/tests/tools/third_party/pytest/src/pytest/collect.py b/tests/wpt/tests/tools/third_party/pytest/src/pytest/collect.py deleted file mode 100644 index 4b2b5818066..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/src/pytest/collect.py +++ /dev/null @@ -1,38 +0,0 @@ -import sys -import warnings -from types import ModuleType -from typing import Any -from typing import List - -import pytest -from _pytest.deprecated import PYTEST_COLLECT_MODULE - -COLLECT_FAKEMODULE_ATTRIBUTES = [ - "Collector", - "Module", - "Function", - "Session", - "Item", - "Class", - "File", - "_fillfuncargs", -] - - -class FakeCollectModule(ModuleType): - def __init__(self) -> None: - super().__init__("pytest.collect") - self.__all__ = list(COLLECT_FAKEMODULE_ATTRIBUTES) - self.__pytest = pytest - - def __dir__(self) -> List[str]: - return dir(super()) + self.__all__ - - def __getattr__(self, name: str) -> Any: - if name not in self.__all__: - raise AttributeError(name) - warnings.warn(PYTEST_COLLECT_MODULE.format(name=name), stacklevel=2) - return getattr(pytest, name) - - -sys.modules["pytest.collect"] = FakeCollectModule() diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/_py/test_local.py b/tests/wpt/tests/tools/third_party/pytest/testing/_py/test_local.py new file mode 100644 index 00000000000..1b5b344551c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/_py/test_local.py @@ -0,0 +1,1581 @@ +# mypy: allow-untyped-defs +import contextlib +import multiprocessing +import os +import sys +import time +from unittest import mock +import warnings + +from py import error +from py.path import local + +import pytest + + +@contextlib.contextmanager +def ignore_encoding_warning(): + with warnings.catch_warnings(): + if sys.version_info > (3, 10): + warnings.simplefilter("ignore", EncodingWarning) + yield + + +class CommonFSTests: + def test_constructor_equality(self, path1): + p = path1.__class__(path1) + assert p == path1 + + def test_eq_nonstring(self, path1): + p1 = path1.join("sampledir") + p2 = path1.join("sampledir") + assert p1 == p2 + + def test_new_identical(self, path1): + assert path1 == path1.new() + + def test_join(self, path1): + p = path1.join("sampledir") + strp = str(p) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + + def test_join_normalized(self, path1): + newpath = path1.join(path1.sep + "sampledir") + strp = str(newpath) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + newpath = path1.join((path1.sep * 2) + "sampledir") + strp = str(newpath) + assert strp.endswith("sampledir") + assert strp.startswith(str(path1)) + + def test_join_noargs(self, path1): + newpath = path1.join() + assert path1 == newpath + + def test_add_something(self, path1): + p = path1.join("sample") + p = p + "dir" + assert p.check() + assert p.exists() + assert p.isdir() + assert not p.isfile() + + def test_parts(self, path1): + newpath = path1.join("sampledir", "otherfile") + par = newpath.parts()[-3:] + assert par == [path1, path1.join("sampledir"), newpath] + + revpar = newpath.parts(reverse=True)[:3] + assert revpar == [newpath, path1.join("sampledir"), path1] + + def test_common(self, path1): + other = path1.join("sampledir") + x = other.common(path1) + assert x == path1 + + # def test_parents_nonexisting_file(self, path1): + # newpath = path1 / 'dirnoexist' / 'nonexisting file' + # par = list(newpath.parents()) + # assert par[:2] == [path1 / 'dirnoexist', path1] + + def test_basename_checks(self, path1): + newpath = path1.join("sampledir") + assert newpath.check(basename="sampledir") + assert newpath.check(notbasename="xyz") + assert newpath.basename == "sampledir" + + def test_basename(self, path1): + newpath = path1.join("sampledir") + assert newpath.check(basename="sampledir") + assert newpath.basename, "sampledir" + + def test_dirname(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirname == str(path1) + + def test_dirpath(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirpath() == path1 + + def test_dirpath_with_args(self, path1): + newpath = path1.join("sampledir") + assert newpath.dirpath("x") == path1.join("x") + + def test_newbasename(self, path1): + newpath = path1.join("samplefile") + newbase = newpath.new(basename="samplefile2") + assert newbase.basename == "samplefile2" + assert newbase.dirpath() == newpath.dirpath() + + def test_not_exists(self, path1): + assert not path1.join("does_not_exist").check() + assert path1.join("does_not_exist").check(exists=0) + + def test_exists(self, path1): + assert path1.join("samplefile").check() + assert path1.join("samplefile").check(exists=1) + assert path1.join("samplefile").exists() + assert path1.join("samplefile").isfile() + assert not path1.join("samplefile").isdir() + + def test_dir(self, path1): + # print repr(path1.join("sampledir")) + assert path1.join("sampledir").check(dir=1) + assert path1.join("samplefile").check(notdir=1) + assert not path1.join("samplefile").check(dir=1) + assert path1.join("samplefile").exists() + assert not path1.join("samplefile").isdir() + assert path1.join("samplefile").isfile() + + def test_fnmatch_file(self, path1): + assert path1.join("samplefile").check(fnmatch="s*e") + assert path1.join("samplefile").fnmatch("s*e") + assert not path1.join("samplefile").fnmatch("s*x") + assert not path1.join("samplefile").check(fnmatch="s*x") + + # def test_fnmatch_dir(self, path1): + + # pattern = path1.sep.join(['s*file']) + # sfile = path1.join("samplefile") + # assert sfile.check(fnmatch=pattern) + + def test_relto(self, path1): + p = path1.join("sampledir", "otherfile") + assert p.relto(path1) == p.sep.join(["sampledir", "otherfile"]) + assert p.check(relto=path1) + assert path1.check(notrelto=p) + assert not path1.check(relto=p) + + def test_bestrelpath(self, path1): + curdir = path1 + sep = curdir.sep + s = curdir.bestrelpath(curdir) + assert s == "." + s = curdir.bestrelpath(curdir.join("hello", "world")) + assert s == "hello" + sep + "world" + + s = curdir.bestrelpath(curdir.dirpath().join("sister")) + assert s == ".." + sep + "sister" + assert curdir.bestrelpath(curdir.dirpath()) == ".." + + assert curdir.bestrelpath("hello") == "hello" + + def test_relto_not_relative(self, path1): + l1 = path1.join("bcde") + l2 = path1.join("b") + assert not l1.relto(l2) + assert not l2.relto(l1) + + def test_listdir(self, path1): + p = path1.listdir() + assert path1.join("sampledir") in p + assert path1.join("samplefile") in p + with pytest.raises(error.ENOTDIR): + path1.join("samplefile").listdir() + + def test_listdir_fnmatchstring(self, path1): + p = path1.listdir("s*dir") + assert len(p) + assert p[0], path1.join("sampledir") + + def test_listdir_filter(self, path1): + p = path1.listdir(lambda x: x.check(dir=1)) + assert path1.join("sampledir") in p + assert path1.join("samplefile") not in p + + def test_listdir_sorted(self, path1): + p = path1.listdir(lambda x: x.check(basestarts="sample"), sort=True) + assert path1.join("sampledir") == p[0] + assert path1.join("samplefile") == p[1] + assert path1.join("samplepickle") == p[2] + + def test_visit_nofilter(self, path1): + lst = [] + for i in path1.visit(): + lst.append(i.relto(path1)) + assert "sampledir" in lst + assert path1.sep.join(["sampledir", "otherfile"]) in lst + + def test_visit_norecurse(self, path1): + lst = [] + for i in path1.visit(None, lambda x: x.basename != "sampledir"): + lst.append(i.relto(path1)) + assert "sampledir" in lst + assert path1.sep.join(["sampledir", "otherfile"]) not in lst + + @pytest.mark.parametrize( + "fil", + ["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")], + ) + def test_visit_filterfunc_is_string(self, path1, fil): + lst = [] + for i in path1.visit(fil): + lst.append(i.relto(path1)) + assert len(lst), 2 + assert "sampledir" in lst + assert "otherdir" in lst + + def test_visit_ignore(self, path1): + p = path1.join("nonexisting") + assert list(p.visit(ignore=error.ENOENT)) == [] + + def test_visit_endswith(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(endswith="file")): + p.append(i.relto(path1)) + assert path1.sep.join(["sampledir", "otherfile"]) in p + assert "samplefile" in p + + def test_cmp(self, path1): + path1 = path1.join("samplefile") + path2 = path1.join("samplefile2") + assert (path1 < path2) == ("samplefile" < "samplefile2") + assert not (path1 < path1) + + def test_simple_read(self, path1): + with ignore_encoding_warning(): + x = path1.join("samplefile").read("r") + assert x == "samplefile\n" + + def test_join_div_operator(self, path1): + newpath = path1 / "/sampledir" / "/test//" + newpath2 = path1.join("sampledir", "test") + assert newpath == newpath2 + + def test_ext(self, path1): + newpath = path1.join("sampledir.ext") + assert newpath.ext == ".ext" + newpath = path1.join("sampledir") + assert not newpath.ext + + def test_purebasename(self, path1): + newpath = path1.join("samplefile.py") + assert newpath.purebasename == "samplefile" + + def test_multiple_parts(self, path1): + newpath = path1.join("samplefile.py") + dirname, purebasename, basename, ext = newpath._getbyspec( + "dirname,purebasename,basename,ext" + ) + assert str(path1).endswith(dirname) # be careful with win32 'drive' + assert purebasename == "samplefile" + assert basename == "samplefile.py" + assert ext == ".py" + + def test_dotted_name_ext(self, path1): + newpath = path1.join("a.b.c") + ext = newpath.ext + assert ext == ".c" + assert newpath.ext == ".c" + + def test_newext(self, path1): + newpath = path1.join("samplefile.py") + newext = newpath.new(ext=".txt") + assert newext.basename == "samplefile.txt" + assert newext.purebasename == "samplefile" + + def test_readlines(self, path1): + fn = path1.join("samplefile") + with ignore_encoding_warning(): + contents = fn.readlines() + assert contents == ["samplefile\n"] + + def test_readlines_nocr(self, path1): + fn = path1.join("samplefile") + with ignore_encoding_warning(): + contents = fn.readlines(cr=0) + assert contents == ["samplefile", ""] + + def test_file(self, path1): + assert path1.join("samplefile").check(file=1) + + def test_not_file(self, path1): + assert not path1.join("sampledir").check(file=1) + assert path1.join("sampledir").check(file=0) + + def test_non_existent(self, path1): + assert path1.join("sampledir.nothere").check(dir=0) + assert path1.join("sampledir.nothere").check(file=0) + assert path1.join("sampledir.nothere").check(notfile=1) + assert path1.join("sampledir.nothere").check(notdir=1) + assert path1.join("sampledir.nothere").check(notexists=1) + assert not path1.join("sampledir.nothere").check(notfile=0) + + # pattern = path1.sep.join(['s*file']) + # sfile = path1.join("samplefile") + # assert sfile.check(fnmatch=pattern) + + def test_size(self, path1): + url = path1.join("samplefile") + assert url.size() > len("samplefile") + + def test_mtime(self, path1): + url = path1.join("samplefile") + assert url.mtime() > 0 + + def test_relto_wrong_type(self, path1): + with pytest.raises(TypeError): + path1.relto(42) + + def test_load(self, path1): + p = path1.join("samplepickle") + obj = p.load() + assert type(obj) is dict + assert obj.get("answer", None) == 42 + + def test_visit_filesonly(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(file=1)): + p.append(i.relto(path1)) + assert "sampledir" not in p + assert path1.sep.join(["sampledir", "otherfile"]) in p + + def test_visit_nodotfiles(self, path1): + p = [] + for i in path1.visit(lambda x: x.check(dotfile=0)): + p.append(i.relto(path1)) + assert "sampledir" in p + assert path1.sep.join(["sampledir", "otherfile"]) in p + assert ".dotfile" not in p + + def test_visit_breadthfirst(self, path1): + lst = [] + for i in path1.visit(bf=True): + lst.append(i.relto(path1)) + for i, p in enumerate(lst): + if path1.sep in p: + for j in range(i, len(lst)): + assert path1.sep in lst[j] + break + else: + pytest.fail("huh") + + def test_visit_sort(self, path1): + lst = [] + for i in path1.visit(bf=True, sort=True): + lst.append(i.relto(path1)) + for i, p in enumerate(lst): + if path1.sep in p: + break + assert lst[:i] == sorted(lst[:i]) + assert lst[i:] == sorted(lst[i:]) + + def test_endswith(self, path1): + def chk(p): + return p.check(endswith="pickle") + + assert not chk(path1) + assert not chk(path1.join("samplefile")) + assert chk(path1.join("somepickle")) + + def test_copy_file(self, path1): + otherdir = path1.join("otherdir") + initpy = otherdir.join("__init__.py") + copied = otherdir.join("copied") + initpy.copy(copied) + try: + assert copied.check() + s1 = initpy.read_text(encoding="utf-8") + s2 = copied.read_text(encoding="utf-8") + assert s1 == s2 + finally: + if copied.check(): + copied.remove() + + def test_copy_dir(self, path1): + otherdir = path1.join("otherdir") + copied = path1.join("newdir") + try: + otherdir.copy(copied) + assert copied.check(dir=1) + assert copied.join("__init__.py").check(file=1) + s1 = otherdir.join("__init__.py").read_text(encoding="utf-8") + s2 = copied.join("__init__.py").read_text(encoding="utf-8") + assert s1 == s2 + finally: + if copied.check(dir=1): + copied.remove(rec=1) + + def test_remove_file(self, path1): + d = path1.ensure("todeleted") + assert d.check() + d.remove() + assert not d.check() + + def test_remove_dir_recursive_by_default(self, path1): + d = path1.ensure("to", "be", "deleted") + assert d.check() + p = path1.join("to") + p.remove() + assert not p.check() + + def test_ensure_dir(self, path1): + b = path1.ensure_dir("001", "002") + assert b.basename == "002" + assert b.isdir() + + def test_mkdir_and_remove(self, path1): + tmpdir = path1 + with pytest.raises(error.EEXIST): + tmpdir.mkdir("sampledir") + new = tmpdir.join("mktest1") + new.mkdir() + assert new.check(dir=1) + new.remove() + + new = tmpdir.mkdir("mktest") + assert new.check(dir=1) + new.remove() + assert tmpdir.join("mktest") == new + + def test_move_file(self, path1): + p = path1.join("samplefile") + newp = p.dirpath("moved_samplefile") + p.move(newp) + try: + assert newp.check(file=1) + assert not p.check() + finally: + dp = newp.dirpath() + if hasattr(dp, "revert"): + dp.revert() + else: + newp.move(p) + assert p.check() + + def test_move_dir(self, path1): + source = path1.join("sampledir") + dest = path1.join("moveddir") + source.move(dest) + assert dest.check(dir=1) + assert dest.join("otherfile").check(file=1) + assert not source.join("sampledir").check() + + def test_fspath_protocol_match_strpath(self, path1): + assert path1.__fspath__() == path1.strpath + + def test_fspath_func_match_strpath(self, path1): + from os import fspath + + assert fspath(path1) == path1.strpath + + @pytest.mark.skip("sys.version_info < (3,6)") + def test_fspath_open(self, path1): + f = path1.join("opentestfile") + open(f) + + @pytest.mark.skip("sys.version_info < (3,6)") + def test_fspath_fsencode(self, path1): + from os import fsencode + + assert fsencode(path1) == fsencode(path1.strpath) + + +def setuptestfs(path): + if path.join("samplefile").check(): + return + # print "setting up test fs for", repr(path) + samplefile = path.ensure("samplefile") + samplefile.write_text("samplefile\n", encoding="utf-8") + + execfile = path.ensure("execfile") + execfile.write_text("x=42", encoding="utf-8") + + execfilepy = path.ensure("execfile.py") + execfilepy.write_text("x=42", encoding="utf-8") + + d = {1: 2, "hello": "world", "answer": 42} + path.ensure("samplepickle").dump(d) + + sampledir = path.ensure("sampledir", dir=1) + sampledir.ensure("otherfile") + + otherdir = path.ensure("otherdir", dir=1) + otherdir.ensure("__init__.py") + + module_a = otherdir.ensure("a.py") + module_a.write_text("from .b import stuff as result\n", encoding="utf-8") + module_b = otherdir.ensure("b.py") + module_b.write_text('stuff="got it"\n', encoding="utf-8") + module_c = otherdir.ensure("c.py") + module_c.write_text( + """import py; +import otherdir.a +value = otherdir.a.result +""", + encoding="utf-8", + ) + module_d = otherdir.ensure("d.py") + module_d.write_text( + """import py; +from otherdir import a +value2 = a.result +""", + encoding="utf-8", + ) + + +win32only = pytest.mark.skipif( + "not (sys.platform == 'win32' or getattr(os, '_name', None) == 'nt')" +) +skiponwin32 = pytest.mark.skipif( + "sys.platform == 'win32' or getattr(os, '_name', None) == 'nt'" +) + +ATIME_RESOLUTION = 0.01 + + +@pytest.fixture(scope="session") +def path1(tmpdir_factory): + path = tmpdir_factory.mktemp("path") + setuptestfs(path) + yield path + assert path.join("samplefile").check() + + +@pytest.fixture +def fake_fspath_obj(request): + class FakeFSPathClass: + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + return FakeFSPathClass(os.path.join("this", "is", "a", "fake", "path")) + + +def batch_make_numbered_dirs(rootdir, repeats): + for i in range(repeats): + dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir) + file_ = dir_.join("foo") + file_.write_text("%s" % i, encoding="utf-8") + actual = int(file_.read_text(encoding="utf-8")) + assert ( + actual == i + ), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}" + dir_.join(".lock").remove(ignore_errors=True) + return True + + +class TestLocalPath(CommonFSTests): + def test_join_normpath(self, tmpdir): + assert tmpdir.join(".") == tmpdir + p = tmpdir.join("../%s" % tmpdir.basename) + assert p == tmpdir + p = tmpdir.join("..//%s/" % tmpdir.basename) + assert p == tmpdir + + @skiponwin32 + def test_dirpath_abs_no_abs(self, tmpdir): + p = tmpdir.join("foo") + assert p.dirpath("/bar") == tmpdir.join("bar") + assert tmpdir.dirpath("/bar", abs=True) == local("/bar") + + def test_gethash(self, tmpdir): + from hashlib import md5 + from hashlib import sha1 as sha + + fn = tmpdir.join("testhashfile") + data = b"hello" + fn.write(data, mode="wb") + assert fn.computehash("md5") == md5(data).hexdigest() + assert fn.computehash("sha1") == sha(data).hexdigest() + with pytest.raises(ValueError): + fn.computehash("asdasd") + + def test_remove_removes_readonly_file(self, tmpdir): + readonly_file = tmpdir.join("readonly").ensure() + readonly_file.chmod(0) + readonly_file.remove() + assert not readonly_file.check(exists=1) + + def test_remove_removes_readonly_dir(self, tmpdir): + readonly_dir = tmpdir.join("readonlydir").ensure(dir=1) + readonly_dir.chmod(int("500", 8)) + readonly_dir.remove() + assert not readonly_dir.check(exists=1) + + def test_remove_removes_dir_and_readonly_file(self, tmpdir): + readonly_dir = tmpdir.join("readonlydir").ensure(dir=1) + readonly_file = readonly_dir.join("readonlyfile").ensure() + readonly_file.chmod(0) + readonly_dir.remove() + assert not readonly_dir.check(exists=1) + + def test_remove_routes_ignore_errors(self, tmpdir, monkeypatch): + lst = [] + monkeypatch.setattr("shutil.rmtree", lambda *args, **kwargs: lst.append(kwargs)) + tmpdir.remove() + assert not lst[0]["ignore_errors"] + for val in (True, False): + lst[:] = [] + tmpdir.remove(ignore_errors=val) + assert lst[0]["ignore_errors"] == val + + def test_initialize_curdir(self): + assert str(local()) == os.getcwd() + + @skiponwin32 + def test_chdir_gone(self, path1): + p = path1.ensure("dir_to_be_removed", dir=1) + p.chdir() + p.remove() + pytest.raises(error.ENOENT, local) + assert path1.chdir() is None + assert os.getcwd() == str(path1) + + with pytest.raises(error.ENOENT): + with p.as_cwd(): + raise NotImplementedError + + @skiponwin32 + def test_chdir_gone_in_as_cwd(self, path1): + p = path1.ensure("dir_to_be_removed", dir=1) + p.chdir() + p.remove() + + with path1.as_cwd() as old: + assert old is None + + def test_as_cwd(self, path1): + dir = path1.ensure("subdir", dir=1) + old = local() + with dir.as_cwd() as x: + assert x == old + assert local() == dir + assert os.getcwd() == str(old) + + def test_as_cwd_exception(self, path1): + old = local() + dir = path1.ensure("subdir", dir=1) + with pytest.raises(ValueError): + with dir.as_cwd(): + raise ValueError() + assert old == local() + + def test_initialize_reldir(self, path1): + with path1.as_cwd(): + p = local("samplefile") + assert p.check() + + def test_tilde_expansion(self, monkeypatch, tmpdir): + monkeypatch.setenv("HOME", str(tmpdir)) + p = local("~", expanduser=True) + assert p == os.path.expanduser("~") + + @pytest.mark.skipif( + not sys.platform.startswith("win32"), reason="case-insensitive only on windows" + ) + def test_eq_hash_are_case_insensitive_on_windows(self): + a = local("/some/path") + b = local("/some/PATH") + assert a == b + assert hash(a) == hash(b) + assert a in {b} + assert a in {b: "b"} + + def test_eq_with_strings(self, path1): + path1 = path1.join("sampledir") + path2 = str(path1) + assert path1 == path2 + assert path2 == path1 + path3 = path1.join("samplefile") + assert path3 != path2 + assert path2 != path3 + + def test_eq_with_none(self, path1): + assert path1 != None # noqa: E711 + + def test_eq_non_ascii_unicode(self, path1): + path2 = path1.join("temp") + path3 = path1.join("ação") + path4 = path1.join("ディレクトリ") + + assert path2 != path3 + assert path2 != path4 + assert path4 != path3 + + def test_gt_with_strings(self, path1): + path2 = path1.join("sampledir") + path3 = str(path1.join("ttt")) + assert path3 > path2 + assert path2 < path3 + assert path2 < "ttt" + assert "ttt" > path2 + path4 = path1.join("aaa") + lst = [path2, path4, path3] + assert sorted(lst) == [path4, path2, path3] + + def test_open_and_ensure(self, path1): + p = path1.join("sub1", "sub2", "file") + with p.open("w", ensure=1, encoding="utf-8") as f: + f.write("hello") + assert p.read_text(encoding="utf-8") == "hello" + + def test_write_and_ensure(self, path1): + p = path1.join("sub1", "sub2", "file") + p.write_text("hello", ensure=1, encoding="utf-8") + assert p.read_text(encoding="utf-8") == "hello" + + @pytest.mark.parametrize("bin", (False, True)) + def test_dump(self, tmpdir, bin): + path = tmpdir.join("dumpfile%s" % int(bin)) + try: + d = {"answer": 42} + path.dump(d, bin=bin) + f = path.open("rb+") + import pickle + + dnew = pickle.load(f) + assert d == dnew + finally: + f.close() + + def test_setmtime(self): + import tempfile + import time + + try: + fd, name = tempfile.mkstemp() + os.close(fd) + except AttributeError: + name = tempfile.mktemp() + open(name, "w").close() + try: + mtime = int(time.time()) - 100 + path = local(name) + assert path.mtime() != mtime + path.setmtime(mtime) + assert path.mtime() == mtime + path.setmtime() + assert path.mtime() != mtime + finally: + os.remove(name) + + def test_normpath(self, path1): + new1 = path1.join("/otherdir") + new2 = path1.join("otherdir") + assert str(new1) == str(new2) + + def test_mkdtemp_creation(self): + d = local.mkdtemp() + try: + assert d.check(dir=1) + finally: + d.remove(rec=1) + + def test_tmproot(self): + d = local.mkdtemp() + tmproot = local.get_temproot() + try: + assert d.check(dir=1) + assert d.dirpath() == tmproot + finally: + d.remove(rec=1) + + def test_chdir(self, tmpdir): + old = local() + try: + res = tmpdir.chdir() + assert str(res) == str(old) + assert os.getcwd() == str(tmpdir) + finally: + old.chdir() + + def test_ensure_filepath_withdir(self, tmpdir): + newfile = tmpdir.join("test1", "test") + newfile.ensure() + assert newfile.check(file=1) + newfile.write_text("42", encoding="utf-8") + newfile.ensure() + s = newfile.read_text(encoding="utf-8") + assert s == "42" + + def test_ensure_filepath_withoutdir(self, tmpdir): + newfile = tmpdir.join("test1file") + t = newfile.ensure() + assert t == newfile + assert newfile.check(file=1) + + def test_ensure_dirpath(self, tmpdir): + newfile = tmpdir.join("test1", "testfile") + t = newfile.ensure(dir=1) + assert t == newfile + assert newfile.check(dir=1) + + def test_ensure_non_ascii_unicode(self, tmpdir): + newfile = tmpdir.join("ação", "ディレクトリ") + t = newfile.ensure(dir=1) + assert t == newfile + assert newfile.check(dir=1) + + @pytest.mark.xfail(run=False, reason="unreliable est for long filenames") + def test_long_filenames(self, tmpdir): + if sys.platform == "win32": + pytest.skip("win32: work around needed for path length limit") + # see http://codespeak.net/pipermail/py-dev/2008q2/000922.html + + # testing paths > 260 chars (which is Windows' limitation, but + # depending on how the paths are used), but > 4096 (which is the + # Linux' limitation) - the behaviour of paths with names > 4096 chars + # is undetermined + newfilename = "/test" * 60 # type:ignore[unreachable,unused-ignore] + l1 = tmpdir.join(newfilename) + l1.ensure(file=True) + l1.write_text("foo", encoding="utf-8") + l2 = tmpdir.join(newfilename) + assert l2.read_text(encoding="utf-8") == "foo" + + def test_visit_depth_first(self, tmpdir): + tmpdir.ensure("a", "1") + tmpdir.ensure("b", "2") + p3 = tmpdir.ensure("breadth") + lst = list(tmpdir.visit(lambda x: x.check(file=1))) + assert len(lst) == 3 + # check that breadth comes last + assert lst[2] == p3 + + def test_visit_rec_fnmatch(self, tmpdir): + p1 = tmpdir.ensure("a", "123") + tmpdir.ensure(".b", "345") + lst = list(tmpdir.visit("???", rec="[!.]*")) + assert len(lst) == 1 + # check that breadth comes last + assert lst[0] == p1 + + def test_fnmatch_file_abspath(self, tmpdir): + b = tmpdir.join("a", "b") + assert b.fnmatch(os.sep.join("ab")) + pattern = os.sep.join([str(tmpdir), "*", "b"]) + assert b.fnmatch(pattern) + + def test_sysfind(self): + name = sys.platform == "win32" and "cmd" or "test" + x = local.sysfind(name) + assert x.check(file=1) + assert local.sysfind("jaksdkasldqwe") is None + assert local.sysfind(name, paths=[]) is None + x2 = local.sysfind(name, paths=[x.dirpath()]) + assert x2 == x + + def test_fspath_protocol_other_class(self, fake_fspath_obj): + # py.path is always absolute + py_path = local(fake_fspath_obj) + str_path = fake_fspath_obj.__fspath__() + assert py_path.check(endswith=str_path) + assert py_path.join(fake_fspath_obj).strpath == os.path.join( + py_path.strpath, str_path + ) + + @pytest.mark.xfail( + reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False + ) + def test_make_numbered_dir_multiprocess_safe(self, tmpdir): + # https://github.com/pytest-dev/py/issues/30 + with multiprocessing.Pool() as pool: + results = [ + pool.apply_async(batch_make_numbered_dirs, [tmpdir, 100]) + for _ in range(20) + ] + for r in results: + assert r.get() + + +class TestExecutionOnWindows: + pytestmark = win32only + + def test_sysfind_bat_exe_before(self, tmpdir, monkeypatch): + monkeypatch.setenv("PATH", str(tmpdir), prepend=os.pathsep) + tmpdir.ensure("hello") + h = tmpdir.ensure("hello.bat") + x = local.sysfind("hello") + assert x == h + + +class TestExecution: + pytestmark = skiponwin32 + + def test_sysfind_no_permission_ignored(self, monkeypatch, tmpdir): + noperm = tmpdir.ensure("noperm", dir=True) + monkeypatch.setenv("PATH", str(noperm), prepend=":") + noperm.chmod(0) + try: + assert local.sysfind("jaksdkasldqwe") is None + finally: + noperm.chmod(0o644) + + def test_sysfind_absolute(self): + x = local.sysfind("test") + assert x.check(file=1) + y = local.sysfind(str(x)) + assert y.check(file=1) + assert y == x + + def test_sysfind_multiple(self, tmpdir, monkeypatch): + monkeypatch.setenv( + "PATH", "{}:{}".format(tmpdir.ensure("a"), tmpdir.join("b")), prepend=":" + ) + tmpdir.ensure("b", "a") + x = local.sysfind("a", checker=lambda x: x.dirpath().basename == "b") + assert x.basename == "a" + assert x.dirpath().basename == "b" + assert local.sysfind("a", checker=lambda x: None) is None + + def test_sysexec(self): + x = local.sysfind("ls") + out = x.sysexec("-a") + for x in local().listdir(): + assert out.find(x.basename) != -1 + + def test_sysexec_failing(self): + try: + from py._process.cmdexec import ExecutionFailed # py library + except ImportError: + ExecutionFailed = RuntimeError # py vendored + x = local.sysfind("false") + with pytest.raises(ExecutionFailed): + x.sysexec("aksjdkasjd") + + def test_make_numbered_dir(self, tmpdir): + tmpdir.ensure("base.not_an_int", dir=1) + for i in range(10): + numdir = local.make_numbered_dir( + prefix="base.", rootdir=tmpdir, keep=2, lock_timeout=0 + ) + assert numdir.check() + assert numdir.basename == "base.%d" % i + if i >= 1: + assert numdir.new(ext=str(i - 1)).check() + if i >= 2: + assert numdir.new(ext=str(i - 2)).check() + if i >= 3: + assert not numdir.new(ext=str(i - 3)).check() + + def test_make_numbered_dir_case(self, tmpdir): + """make_numbered_dir does not make assumptions on the underlying + filesystem based on the platform and will assume it _could_ be case + insensitive. + + See issues: + - https://github.com/pytest-dev/pytest/issues/708 + - https://github.com/pytest-dev/pytest/issues/3451 + """ + d1 = local.make_numbered_dir( + prefix="CAse.", + rootdir=tmpdir, + keep=2, + lock_timeout=0, + ) + d2 = local.make_numbered_dir( + prefix="caSE.", + rootdir=tmpdir, + keep=2, + lock_timeout=0, + ) + assert str(d1).lower() != str(d2).lower() + assert str(d2).endswith(".1") + + def test_make_numbered_dir_NotImplemented_Error(self, tmpdir, monkeypatch): + def notimpl(x, y): + raise NotImplementedError(42) + + monkeypatch.setattr(os, "symlink", notimpl) + x = tmpdir.make_numbered_dir(rootdir=tmpdir, lock_timeout=0) + assert x.relto(tmpdir) + assert x.check() + + def test_locked_make_numbered_dir(self, tmpdir): + for i in range(10): + numdir = local.make_numbered_dir(prefix="base2.", rootdir=tmpdir, keep=2) + assert numdir.check() + assert numdir.basename == "base2.%d" % i + for j in range(i): + assert numdir.new(ext=str(j)).check() + + def test_error_preservation(self, path1): + pytest.raises(EnvironmentError, path1.join("qwoeqiwe").mtime) + pytest.raises(EnvironmentError, path1.join("qwoeqiwe").read) + + # def test_parentdirmatch(self): + # local.parentdirmatch('std', startmodule=__name__) + # + + +class TestImport: + @pytest.fixture(autouse=True) + def preserve_sys(self): + with mock.patch.dict(sys.modules): + with mock.patch.object(sys, "path", list(sys.path)): + yield + + def test_pyimport(self, path1): + obj = path1.join("execfile.py").pyimport() + assert obj.x == 42 + assert obj.__name__ == "execfile" + + def test_pyimport_renamed_dir_creates_mismatch(self, tmpdir, monkeypatch): + p = tmpdir.ensure("a", "test_x123.py") + p.pyimport() + tmpdir.join("a").move(tmpdir.join("b")) + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() + + # Errors can be ignored. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") + tmpdir.join("b", "test_x123.py").pyimport() + + # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. + monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") + with pytest.raises(tmpdir.ImportMismatchError): + tmpdir.join("b", "test_x123.py").pyimport() + + def test_pyimport_messy_name(self, tmpdir): + # http://bitbucket.org/hpk42/py-trunk/issue/129 + path = tmpdir.ensure("foo__init__.py") + path.pyimport() + + def test_pyimport_dir(self, tmpdir): + p = tmpdir.join("hello_123") + p_init = p.ensure("__init__.py") + m = p.pyimport() + assert m.__name__ == "hello_123" + m = p_init.pyimport() + assert m.__name__ == "hello_123" + + def test_pyimport_execfile_different_name(self, path1): + obj = path1.join("execfile.py").pyimport(modname="0x.y.z") + assert obj.x == 42 + assert obj.__name__ == "0x.y.z" + + def test_pyimport_a(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("a.py").pyimport() + assert mod.result == "got it" + assert mod.__name__ == "otherdir.a" + + def test_pyimport_b(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("b.py").pyimport() + assert mod.stuff == "got it" + assert mod.__name__ == "otherdir.b" + + def test_pyimport_c(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("c.py").pyimport() + assert mod.value == "got it" + + def test_pyimport_d(self, path1): + otherdir = path1.join("otherdir") + mod = otherdir.join("d.py").pyimport() + assert mod.value2 == "got it" + + def test_pyimport_and_import(self, tmpdir): + tmpdir.ensure("xxxpackage", "__init__.py") + mod1path = tmpdir.ensure("xxxpackage", "module1.py") + mod1 = mod1path.pyimport() + assert mod1.__name__ == "xxxpackage.module1" + from xxxpackage import module1 + + assert module1 is mod1 + + def test_pyimport_check_filepath_consistency(self, monkeypatch, tmpdir): + name = "pointsback123" + ModuleType = type(os) + p = tmpdir.ensure(name + ".py") + with monkeypatch.context() as mp: + for ending in (".pyc", "$py.class", ".pyo"): + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + ending) + mod.__file__ = str(pseudopath) + mp.setitem(sys.modules, name, mod) + newmod = p.pyimport() + assert mod == newmod + mod = ModuleType(name) + pseudopath = tmpdir.ensure(name + "123.py") + mod.__file__ = str(pseudopath) + monkeypatch.setitem(sys.modules, name, mod) + excinfo = pytest.raises(pseudopath.ImportMismatchError, p.pyimport) + modname, modfile, orig = excinfo.value.args + assert modname == name + assert modfile == pseudopath + assert orig == p + assert issubclass(pseudopath.ImportMismatchError, ImportError) + + def test_issue131_pyimport_on__init__(self, tmpdir): + # __init__.py files may be namespace packages, and thus the + # __file__ of an imported module may not be ourselves + # see issue + p1 = tmpdir.ensure("proja", "__init__.py") + p2 = tmpdir.ensure("sub", "proja", "__init__.py") + m1 = p1.pyimport() + m2 = p2.pyimport() + assert m1 == m2 + + def test_ensuresyspath_append(self, tmpdir): + root1 = tmpdir.mkdir("root1") + file1 = root1.ensure("x123.py") + assert str(root1) not in sys.path + file1.pyimport(ensuresyspath="append") + assert str(root1) == sys.path[-1] + assert str(root1) not in sys.path[:-1] + + +class TestImportlibImport: + OPTS = {"ensuresyspath": "importlib"} + + def test_pyimport(self, path1): + obj = path1.join("execfile.py").pyimport(**self.OPTS) + assert obj.x == 42 + assert obj.__name__ == "execfile" + + def test_pyimport_dir_fails(self, tmpdir): + p = tmpdir.join("hello_123") + p.ensure("__init__.py") + with pytest.raises(ImportError): + p.pyimport(**self.OPTS) + + def test_pyimport_execfile_different_name(self, path1): + obj = path1.join("execfile.py").pyimport(modname="0x.y.z", **self.OPTS) + assert obj.x == 42 + assert obj.__name__ == "0x.y.z" + + def test_pyimport_relative_import_fails(self, path1): + otherdir = path1.join("otherdir") + with pytest.raises(ImportError): + otherdir.join("a.py").pyimport(**self.OPTS) + + def test_pyimport_doesnt_use_sys_modules(self, tmpdir): + p = tmpdir.ensure("file738jsk.py") + mod = p.pyimport(**self.OPTS) + assert mod.__name__ == "file738jsk" + assert "file738jsk" not in sys.modules + + +def test_pypkgdir(tmpdir): + pkg = tmpdir.ensure("pkg1", dir=1) + pkg.ensure("__init__.py") + pkg.ensure("subdir/__init__.py") + assert pkg.pypkgpath() == pkg + assert pkg.join("subdir", "__init__.py").pypkgpath() == pkg + + +def test_pypkgdir_unimportable(tmpdir): + pkg = tmpdir.ensure("pkg1-1", dir=1) # unimportable + pkg.ensure("__init__.py") + subdir = pkg.ensure("subdir/__init__.py").dirpath() + assert subdir.pypkgpath() == subdir + assert subdir.ensure("xyz.py").pypkgpath() == subdir + assert not pkg.pypkgpath() + + +def test_isimportable(): + try: + from py.path import isimportable # py vendored version + except ImportError: + from py._path.local import isimportable # py library + + assert not isimportable("") + assert isimportable("x") + assert isimportable("x1") + assert isimportable("x_1") + assert isimportable("_") + assert isimportable("_1") + assert not isimportable("x-1") + assert not isimportable("x:1") + + +def test_homedir_from_HOME(monkeypatch): + path = os.getcwd() + monkeypatch.setenv("HOME", path) + assert local._gethomedir() == local(path) + + +def test_homedir_not_exists(monkeypatch): + monkeypatch.delenv("HOME", raising=False) + monkeypatch.delenv("HOMEDRIVE", raising=False) + homedir = local._gethomedir() + assert homedir is None + + +def test_samefile(tmpdir): + assert tmpdir.samefile(tmpdir) + p = tmpdir.ensure("hello") + assert p.samefile(p) + with p.dirpath().as_cwd(): + assert p.samefile(p.basename) + if sys.platform == "win32": + p1 = p.__class__(str(p).lower()) + p2 = p.__class__(str(p).upper()) + assert p1.samefile(p2) + + +@pytest.mark.skipif(not hasattr(os, "symlink"), reason="os.symlink not available") +def test_samefile_symlink(tmpdir): + p1 = tmpdir.ensure("foo.txt") + p2 = tmpdir.join("linked.txt") + try: + os.symlink(str(p1), str(p2)) + except (OSError, NotImplementedError) as e: + # on Windows this might fail if the user doesn't have special symlink permissions + # pypy3 on Windows doesn't implement os.symlink and raises NotImplementedError + pytest.skip(str(e.args[0])) + + assert p1.samefile(p2) + + +def test_listdir_single_arg(tmpdir): + tmpdir.ensure("hello") + assert tmpdir.listdir("hello")[0].basename == "hello" + + +def test_mkdtemp_rootdir(tmpdir): + dtmp = local.mkdtemp(rootdir=tmpdir) + assert tmpdir.listdir() == [dtmp] + + +class TestWINLocalPath: + pytestmark = win32only + + def test_owner_group_not_implemented(self, path1): + with pytest.raises(NotImplementedError): + _ = path1.stat().owner + with pytest.raises(NotImplementedError): + _ = path1.stat().group + + def test_chmod_simple_int(self, path1): + mode = path1.stat().mode + # Ensure that we actually change the mode to something different. + path1.chmod(mode == 0 and 1 or 0) + try: + print(path1.stat().mode) + print(mode) + assert path1.stat().mode != mode + finally: + path1.chmod(mode) + assert path1.stat().mode == mode + + def test_path_comparison_lowercase_mixed(self, path1): + t1 = path1.join("a_path") + t2 = path1.join("A_path") + assert t1 == t1 + assert t1 == t2 + + def test_relto_with_mixed_case(self, path1): + t1 = path1.join("a_path", "fiLe") + t2 = path1.join("A_path") + assert t1.relto(t2) == "fiLe" + + def test_allow_unix_style_paths(self, path1): + t1 = path1.join("a_path") + assert t1 == str(path1) + "\\a_path" + t1 = path1.join("a_path/") + assert t1 == str(path1) + "\\a_path" + t1 = path1.join("dir/a_path") + assert t1 == str(path1) + "\\dir\\a_path" + + def test_sysfind_in_currentdir(self, path1): + cmd = local.sysfind("cmd") + root = cmd.new(dirname="", basename="") # c:\ in most installations + with root.as_cwd(): + x = local.sysfind(cmd.relto(root)) + assert x.check(file=1) + + def test_fnmatch_file_abspath_posix_pattern_on_win32(self, tmpdir): + # path-matching patterns might contain a posix path separator '/' + # Test that we can match that pattern on windows. + import posixpath + + b = tmpdir.join("a", "b") + assert b.fnmatch(posixpath.sep.join("ab")) + pattern = posixpath.sep.join([str(tmpdir), "*", "b"]) + assert b.fnmatch(pattern) + + +class TestPOSIXLocalPath: + pytestmark = skiponwin32 + + def test_hardlink(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write_text("Hello", encoding="utf-8") + nlink = filepath.stat().nlink + linkpath.mklinkto(filepath) + assert filepath.stat().nlink == nlink + 1 + + def test_symlink_are_identical(self, tmpdir): + filepath = tmpdir.join("file") + filepath.write_text("Hello", encoding="utf-8") + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(filepath) + assert linkpath.readlink() == str(filepath) + + def test_symlink_isfile(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write_text("", encoding="utf-8") + linkpath.mksymlinkto(filepath) + assert linkpath.check(file=1) + assert not linkpath.check(link=0, file=1) + assert linkpath.islink() + + def test_symlink_relative(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write_text("Hello", encoding="utf-8") + linkpath.mksymlinkto(filepath, absolute=False) + assert linkpath.readlink() == "file" + assert filepath.read_text(encoding="utf-8") == linkpath.read_text( + encoding="utf-8" + ) + + def test_symlink_not_existing(self, tmpdir): + linkpath = tmpdir.join("testnotexisting") + assert not linkpath.check(link=1) + assert linkpath.check(link=0) + + def test_relto_with_root(self, path1, tmpdir): + y = path1.join("x").relto(local("/")) + assert y[0] == str(path1)[1] + + def test_visit_recursive_symlink(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(tmpdir) + visitor = tmpdir.visit(None, lambda x: x.check(link=0)) + assert list(visitor) == [linkpath] + + def test_symlink_isdir(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(tmpdir) + assert linkpath.check(dir=1) + assert not linkpath.check(link=0, dir=1) + + def test_symlink_remove(self, tmpdir): + linkpath = tmpdir.join("test") + linkpath.mksymlinkto(linkpath) # point to itself + assert linkpath.check(link=1) + linkpath.remove() + assert not linkpath.check() + + def test_realpath_file(self, tmpdir): + linkpath = tmpdir.join("test") + filepath = tmpdir.join("file") + filepath.write_text("", encoding="utf-8") + linkpath.mksymlinkto(filepath) + realpath = linkpath.realpath() + assert realpath.basename == "file" + + def test_owner(self, path1, tmpdir): + from grp import getgrgid # type:ignore[attr-defined,unused-ignore] + from pwd import getpwuid # type:ignore[attr-defined,unused-ignore] + + stat = path1.stat() + assert stat.path == path1 + + uid = stat.uid + gid = stat.gid + owner = getpwuid(uid)[0] + group = getgrgid(gid)[0] + + assert uid == stat.uid + assert owner == stat.owner + assert gid == stat.gid + assert group == stat.group + + def test_stat_helpers(self, tmpdir, monkeypatch): + path1 = tmpdir.ensure("file") + stat1 = path1.stat() + stat2 = tmpdir.stat() + assert stat1.isfile() + assert stat2.isdir() + assert not stat1.islink() + assert not stat2.islink() + + def test_stat_non_raising(self, tmpdir): + path1 = tmpdir.join("file") + pytest.raises(error.ENOENT, lambda: path1.stat()) + res = path1.stat(raising=False) + assert res is None + + def test_atime(self, tmpdir): + import time + + path = tmpdir.ensure("samplefile") + now = time.time() + atime1 = path.atime() + # we could wait here but timer resolution is very + # system dependent + path.read_binary() + time.sleep(ATIME_RESOLUTION) + atime2 = path.atime() + time.sleep(ATIME_RESOLUTION) + duration = time.time() - now + assert (atime2 - atime1) <= duration + + def test_commondir(self, path1): + # XXX This is here in local until we find a way to implement this + # using the subversion command line api. + p1 = path1.join("something") + p2 = path1.join("otherthing") + assert p1.common(p2) == path1 + assert p2.common(p1) == path1 + + def test_commondir_nocommon(self, path1): + # XXX This is here in local until we find a way to implement this + # using the subversion command line api. + p1 = path1.join("something") + p2 = local(path1.sep + "blabla") + assert p1.common(p2) == "/" + + def test_join_to_root(self, path1): + root = path1.parts()[0] + assert len(str(root)) == 1 + assert str(root.join("a")) == "/a" + + def test_join_root_to_root_with_no_abs(self, path1): + nroot = path1.join("/") + assert str(path1) == str(nroot) + assert path1 == nroot + + def test_chmod_simple_int(self, path1): + mode = path1.stat().mode + path1.chmod(int(mode / 2)) + try: + assert path1.stat().mode != mode + finally: + path1.chmod(mode) + assert path1.stat().mode == mode + + def test_chmod_rec_int(self, path1): + # XXX fragile test + def recfilter(x): + return x.check(dotfile=0, link=0) + + oldmodes = {} + for x in path1.visit(rec=recfilter): + oldmodes[x] = x.stat().mode + path1.chmod(int("772", 8), rec=recfilter) + try: + for x in path1.visit(rec=recfilter): + assert x.stat().mode & int("777", 8) == int("772", 8) + finally: + for x, y in oldmodes.items(): + x.chmod(y) + + def test_copy_archiving(self, tmpdir): + unicode_fn = "something-\342\200\223.txt" + f = tmpdir.ensure("a", unicode_fn) + a = f.dirpath() + oldmode = f.stat().mode + newmode = oldmode ^ 1 + f.chmod(newmode) + b = tmpdir.join("b") + a.copy(b, mode=True) + assert b.join(f.basename).stat().mode == newmode + + def test_copy_stat_file(self, tmpdir): + src = tmpdir.ensure("src") + dst = tmpdir.join("dst") + # a small delay before the copy + time.sleep(ATIME_RESOLUTION) + src.copy(dst, stat=True) + oldstat = src.stat() + newstat = dst.stat() + assert oldstat.mode == newstat.mode + assert (dst.atime() - src.atime()) < ATIME_RESOLUTION + assert (dst.mtime() - src.mtime()) < ATIME_RESOLUTION + + def test_copy_stat_dir(self, tmpdir): + test_files = ["a", "b", "c"] + src = tmpdir.join("src") + for f in test_files: + src.join(f).write_text(f, ensure=True, encoding="utf-8") + dst = tmpdir.join("dst") + # a small delay before the copy + time.sleep(ATIME_RESOLUTION) + src.copy(dst, stat=True) + for f in test_files: + oldstat = src.join(f).stat() + newstat = dst.join(f).stat() + assert (newstat.atime - oldstat.atime) < ATIME_RESOLUTION + assert (newstat.mtime - oldstat.mtime) < ATIME_RESOLUTION + assert oldstat.mode == newstat.mode + + def test_chown_identity(self, path1): + owner = path1.stat().owner + group = path1.stat().group + path1.chown(owner, group) + + def test_chown_dangling_link(self, path1): + owner = path1.stat().owner + group = path1.stat().group + x = path1.join("hello") + x.mksymlinkto("qlwkejqwlek") + try: + path1.chown(owner, group, rec=1) + finally: + x.remove(rec=0) + + def test_chown_identity_rec_mayfail(self, path1): + owner = path1.stat().owner + group = path1.stat().group + path1.chown(owner, group) + + +class TestUnicode: + def test_join_ensure(self, tmpdir, monkeypatch): + if "LANG" not in os.environ: + pytest.skip("cannot run test without locale") + x = local(tmpdir.strpath) + part = "hällo" + y = x.ensure(part) + assert x.join(part) == y + + def test_listdir(self, tmpdir): + if "LANG" not in os.environ: + pytest.skip("cannot run test without locale") + x = local(tmpdir.strpath) + part = "hällo" + y = x.ensure(part) + assert x.listdir(part)[0] == y + + @pytest.mark.xfail(reason="changing read/write might break existing usages") + def test_read_write(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + with ignore_encoding_warning(): + x.write(part) + assert x.read() == part + x.write(part.encode(sys.getdefaultencoding())) + assert x.read() == part.encode(sys.getdefaultencoding()) + + +class TestBinaryAndTextMethods: + def test_read_binwrite(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + part_utf8 = part.encode("utf8") + x.write_binary(part_utf8) + assert x.read_binary() == part_utf8 + s = x.read_text(encoding="utf8") + assert s == part + assert isinstance(s, str) + + def test_read_textwrite(self, tmpdir): + x = tmpdir.join("hello") + part = "hällo" + part_utf8 = part.encode("utf8") + x.write_text(part, encoding="utf8") + assert x.read_binary() == part_utf8 + assert x.read_text(encoding="utf8") == part + + def test_default_encoding(self, tmpdir): + x = tmpdir.join("hello") + # Can't use UTF8 as the default encoding (ASCII) doesn't support it + part = "hello" + x.write_text(part, "ascii") + s = x.read_text("ascii") + assert s == part + assert type(s) is type(part) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/acceptance_test.py b/tests/wpt/tests/tools/third_party/pytest/testing/acceptance_test.py index 8b8d4a4a6ed..8f001bc2401 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/acceptance_test.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/acceptance_test.py @@ -1,14 +1,16 @@ +# mypy: allow-untyped-defs +import dataclasses +import importlib.metadata import os +from pathlib import Path +import subprocess import sys import types -import attr - -import pytest -from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester +import pytest def prepend_pythonpath(*dirs) -> str: @@ -115,11 +117,11 @@ class TestGeneralUsage: loaded = [] - @attr.s + @dataclasses.dataclass class DummyEntryPoint: - name = attr.ib() - module = attr.ib() - group = "pytest11" + name: str + module: str + group: str = "pytest11" def load(self): __import__(self.module) @@ -132,15 +134,15 @@ class TestGeneralUsage: DummyEntryPoint("mycov", "mycov_module"), ] - @attr.s + @dataclasses.dataclass class DummyDist: - entry_points = attr.ib() - files = () + entry_points: object + files: object = () def my_dists(): return (DummyDist(entry_points),) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () pytester.runpytest_inprocess(*params) if load_cov_early: @@ -187,7 +189,7 @@ class TestGeneralUsage: result.stderr.fnmatch_lines( [ f"ERROR: not found: {p2}", - f"(no name {str(p2)!r} in any of [[][]])", + "(no match in any of *)", "", ] ) @@ -269,7 +271,7 @@ class TestGeneralUsage: def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None: sub1 = pytester.mkdir("sub1") sub2 = pytester.mkdir("sub2") - sub1.joinpath("conftest.py").write_text("assert 0") + sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8") result = pytester.runpytest(sub2) assert result.ret == ExitCode.NO_TESTS_COLLECTED sub2.joinpath("__init__.py").touch() @@ -343,6 +345,45 @@ class TestGeneralUsage: assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) + def test_direct_addressing_selects_duplicates(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", [1, 2, 10, 11, 2, 1, 12, 11]) + def test_func(a): + pass + """ + ) + result = pytester.runpytest(p) + result.assert_outcomes(failed=0, passed=8) + + def test_direct_addressing_selects_duplicates_1(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", [1, 2, 10, 11, 2, 1, 12, 1_1,2_1]) + def test_func(a): + pass + """ + ) + result = pytester.runpytest(p) + result.assert_outcomes(failed=0, passed=9) + + def test_direct_addressing_selects_duplicates_2(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", ["a","b","c","a","a1"]) + def test_func(a): + pass + """ + ) + result = pytester.runpytest(p) + result.assert_outcomes(failed=0, passed=5) + def test_direct_addressing_notfound(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ @@ -469,7 +510,7 @@ class TestGeneralUsage: assert "invalid" in str(excinfo.value) p = pytester.path.joinpath("test_test_plugins_given_as_strings.py") - p.write_text("def test_foo(): pass") + p.write_text("def test_foo(): pass", encoding="utf-8") mod = types.ModuleType("myplugin") monkeypatch.setitem(sys.modules, "myplugin", mod) assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0 @@ -501,6 +542,32 @@ class TestGeneralUsage: res = pytester.runpytest(p) res.assert_outcomes(passed=3) + # Warning ignore because of: + # https://github.com/python/cpython/issues/85308 + # Can be removed once Python<3.12 support is dropped. + @pytest.mark.filterwarnings("ignore:'encoding' argument not specified") + def test_command_line_args_from_file( + self, pytester: Pytester, tmp_path: Path + ) -> None: + pytester.makepyfile( + test_file=""" + import pytest + + class TestClass: + @pytest.mark.parametrize("a", ["x","y"]) + def test_func(self, a): + pass + """ + ) + tests = [ + "test_file.py::TestClass::test_func[x]", + "test_file.py::TestClass::test_func[y]", + "-q", + ] + args_file = pytester.maketxtfile(tests="\n".join(tests)) + result = pytester.runpytest(f"@{args_file}") + result.assert_outcomes(failed=0, passed=2) + class TestInvocationVariants: def test_earlyinit(self, pytester: Pytester) -> None: @@ -589,7 +656,7 @@ class TestInvocationVariants: def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None: monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) path = pytester.mkpydir("tpkg") - path.joinpath("test_hello.py").write_text("raise ImportError") + path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8") result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) assert result.ret != 0 @@ -599,10 +666,10 @@ class TestInvocationVariants: def test_pyargs_only_imported_once(self, pytester: Pytester) -> None: pkg = pytester.mkpydir("foo") pkg.joinpath("test_foo.py").write_text( - "print('hello from test_foo')\ndef test(): pass" + "print('hello from test_foo')\ndef test(): pass", encoding="utf-8" ) pkg.joinpath("conftest.py").write_text( - "def pytest_configure(config): print('configuring')" + "def pytest_configure(config): print('configuring')", encoding="utf-8" ) result = pytester.runpytest( @@ -615,7 +682,7 @@ class TestInvocationVariants: def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None: pytester.path.joinpath("conftest.py").touch() - pytester.path.joinpath("t.py").write_text("def test(): pass") + pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8") result = pytester.runpytest("--pyargs", "t.py") assert result.ret == ExitCode.OK @@ -624,8 +691,12 @@ class TestInvocationVariants: monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) path = pytester.mkpydir("tpkg") - path.joinpath("test_hello.py").write_text("def test_hello(): pass") - path.joinpath("test_world.py").write_text("def test_world(): pass") + path.joinpath("test_hello.py").write_text( + "def test_hello(): pass", encoding="utf-8" + ) + path.joinpath("test_world.py").write_text( + "def test_world(): pass", encoding="utf-8" + ) result = pytester.runpytest("--pyargs", "tpkg") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) @@ -664,13 +735,15 @@ class TestInvocationVariants: ns = d.joinpath("ns_pkg") ns.mkdir() ns.joinpath("__init__.py").write_text( - "__import__('pkg_resources').declare_namespace(__name__)" + "__import__('pkg_resources').declare_namespace(__name__)", + encoding="utf-8", ) lib = ns.joinpath(dirname) lib.mkdir() lib.joinpath("__init__.py").touch() lib.joinpath(f"test_{dirname}.py").write_text( - f"def test_{dirname}(): pass\ndef test_other():pass" + f"def test_{dirname}(): pass\ndef test_other():pass", + encoding="utf-8", ) # The structure of the test directory is now: @@ -695,7 +768,18 @@ class TestInvocationVariants: # mixed module and filenames: monkeypatch.chdir("world") - result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world") + + # pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages. + # pgk_resources has been deprecated entirely. + # While we could change the test to use implicit namespace packages, seems better + # to still ensure the old declaration via declare_namespace still works. + ignore_w = ( + r"-Wignore:Deprecated call to `pkg_resources.declare_namespace", + r"-Wignore:pkg_resources is deprecated", + ) + result = pytester.runpytest( + "--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w + ) assert result.ret == 0 result.stdout.fnmatch_lines( [ @@ -745,10 +829,10 @@ class TestInvocationVariants: lib.mkdir() lib.joinpath("__init__.py").touch() lib.joinpath("test_bar.py").write_text( - "def test_bar(): pass\ndef test_other(a_fixture):pass" + "def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8" ) lib.joinpath("conftest.py").write_text( - "import pytest\n@pytest.fixture\ndef a_fixture():pass" + "import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8" ) d_local = pytester.mkdir("symlink_root") @@ -873,7 +957,6 @@ class TestDurations: ) def test_calls_show_2(self, pytester: Pytester, mock_timing) -> None: - pytester.makepyfile(self.source) result = pytester.runpytest_inprocess("--durations=2") assert result.ret == 0 @@ -1038,14 +1121,14 @@ def test_fixture_values_leak(pytester: Pytester) -> None: """ pytester.makepyfile( """ - import attr + import dataclasses import gc import pytest import weakref - @attr.s - class SomeObj(object): - name = attr.ib() + @dataclasses.dataclass + class SomeObj: + name: str fix_of_test1_ref = None session_ref = None @@ -1150,7 +1233,6 @@ def test_usage_error_code(pytester: Pytester) -> None: assert result.ret == ExitCode.USAGE_ERROR -@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning") def test_warn_on_async_function(pytester: Pytester) -> None: # In the below we .close() the coroutine only to avoid # "RuntimeWarning: coroutine 'test_2' was never awaited" @@ -1167,7 +1249,7 @@ def test_warn_on_async_function(pytester: Pytester) -> None: return coro """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wdefault") result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1183,7 +1265,6 @@ def test_warn_on_async_function(pytester: Pytester) -> None: ) -@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning") def test_warn_on_async_gen_function(pytester: Pytester) -> None: pytester.makepyfile( test_async=""" @@ -1195,7 +1276,7 @@ def test_warn_on_async_gen_function(pytester: Pytester) -> None: return test_2() """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wdefault") result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1238,8 +1319,6 @@ def test_pdb_can_be_rewritten(pytester: Pytester) -> None: " def check():", "> assert 1 == 2", "E assert 1 == 2", - "E +1", - "E -2", "", "pdb.py:2: AssertionError", "*= 1 failed in *", @@ -1270,8 +1349,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None: result.stderr.fnmatch_lines(["*@this is stderr@*"]) # now ensure the output is in the junitxml - with open(pytester.path.joinpath("output.xml")) as f: - fullXml = f.read() + fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8") assert "@this is stdout@\n" in fullXml assert "@this is stderr@\n" in fullXml @@ -1295,3 +1373,107 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None: # Cleanup. popen.stderr.close() + + +def test_function_return_non_none_warning(pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_stuff(): + return "something" + """ + ) + res = pytester.runpytest() + res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"]) + + +def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None: + """ + Regression test for #10811: previously import_path with ImportMode.importlib would + not return a module if already in sys.modules, resulting in modules being imported + multiple times, which causes problems with modules that have import side effects. + """ + # Uses the exact reproducer form #10811, given it is very minimal + # and illustrates the problem well. + pytester.makepyfile( + **{ + "pmxbot/commands.py": "from . import logging", + "pmxbot/logging.py": "", + "tests/__init__.py": "", + "tests/test_commands.py": """ + import importlib + from pmxbot import logging + + class TestCommands: + def test_boo(self): + assert importlib.import_module('pmxbot.logging') is logging + """, + } + ) + pytester.makeini( + """ + [pytest] + addopts= + --doctest-modules + --import-mode importlib + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines("*1 passed*") + + +@pytest.mark.skip(reason="Test is not isolated") +def test_issue_9765(pytester: Pytester) -> None: + """Reproducer for issue #9765 on Windows + + https://github.com/pytest-dev/pytest/issues/9765 + """ + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + addopts = "-p my_package.plugin.my_plugin" + """ + ) + pytester.makepyfile( + **{ + "setup.py": ( + """ + from setuptools import setup + + if __name__ == '__main__': + setup(name='my_package', packages=['my_package', 'my_package.plugin']) + """ + ), + "my_package/__init__.py": "", + "my_package/conftest.py": "", + "my_package/test_foo.py": "def test(): pass", + "my_package/plugin/__init__.py": "", + "my_package/plugin/my_plugin.py": ( + """ + import pytest + + def pytest_configure(config): + + class SimplePlugin: + @pytest.fixture(params=[1, 2, 3]) + def my_fixture(self, request): + yield request.param + + config.pluginmanager.register(SimplePlugin()) + """ + ), + } + ) + + subprocess.run([sys.executable, "setup.py", "develop"], check=True) + try: + # We are using subprocess.run rather than pytester.run on purpose. + # pytester.run is adding the current directory to PYTHONPATH which avoids + # the bug. We also use pytest rather than python -m pytest for the same + # PYTHONPATH reason. + subprocess.run( + ["pytest", "my_package"], capture_output=True, check=True, text=True + ) + except subprocess.CalledProcessError as exc: + raise AssertionError( + f"pytest command failed:\n{exc.stdout=!s}\n{exc.stderr=!s}" + ) from exc diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_code.py b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_code.py index 33809528a06..57ab4cdfddb 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_code.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_code.py @@ -1,15 +1,16 @@ +# mypy: allow-untyped-defs import re import sys from types import FrameType from unittest import mock -import pytest from _pytest._code import Code from _pytest._code import ExceptionInfo from _pytest._code import Frame from _pytest._code import Source from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ReprFuncArgs +import pytest def test_ne() -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_excinfo.py b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_excinfo.py index 61aa4406ad2..e95510f92d6 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_excinfo.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_excinfo.py @@ -1,18 +1,19 @@ +# mypy: allow-untyped-defs +from __future__ import annotations + +import fnmatch import importlib import io import operator +from pathlib import Path import queue +import re import sys import textwrap -from pathlib import Path from typing import Any -from typing import Dict -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union -import _pytest -import pytest +import _pytest._code from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import FormattedExcinfo @@ -22,11 +23,15 @@ from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester +import pytest if TYPE_CHECKING: from _pytest._code.code import _TracebackStyle +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + @pytest.fixture def limited_recursion_depth(): @@ -53,6 +58,20 @@ def test_excinfo_from_exc_info_simple() -> None: assert info.type == ValueError +def test_excinfo_from_exception_simple() -> None: + try: + raise ValueError + except ValueError as e: + assert e.__traceback__ is not None + info = _pytest._code.ExceptionInfo.from_exception(e) + assert info.type == ValueError + + +def test_excinfo_from_exception_missing_traceback_assertion() -> None: + with pytest.raises(AssertionError, match=r"must have.*__traceback__"): + _pytest._code.ExceptionInfo.from_exception(ValueError()) + + def test_excinfo_getstatement(): def g(): raise ValueError @@ -162,7 +181,7 @@ class TestTraceback_f_g_h: def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: p = pytester.makepyfile("def f(): raise ValueError") with pytest.raises(ValueError) as excinfo: - import_path(p, root=pytester.path).f() # type: ignore[attr-defined] + import_path(p, root=pytester.path, consider_namespace_packages=False).f() basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: @@ -172,7 +191,7 @@ class TestTraceback_f_g_h: def test_traceback_filter(self): traceback = self.excinfo.traceback - ntraceback = traceback.filter() + ntraceback = traceback.filter(self.excinfo) assert len(ntraceback) == len(traceback) - 1 @pytest.mark.parametrize( @@ -203,7 +222,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, h) traceback = excinfo.traceback - ntraceback = traceback.filter() + ntraceback = traceback.filter(excinfo) print(f"old: {traceback!r}") print(f"new: {ntraceback!r}") @@ -219,7 +238,7 @@ class TestTraceback_f_g_h: n += 1 f(n) - excinfo = pytest.raises(RuntimeError, f, 8) + excinfo = pytest.raises(RecursionError, f, 8) traceback = excinfo.traceback recindex = traceback.recursionindex() assert recindex == 3 @@ -276,7 +295,7 @@ class TestTraceback_f_g_h: excinfo = pytest.raises(ValueError, fail) assert excinfo.traceback.recursionindex() is None - def test_traceback_getcrashentry(self): + def test_getreprcrash(self): def i(): __tracebackhide__ = True raise ValueError @@ -292,14 +311,13 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry() + reprcrash = excinfo._getreprcrash() + assert reprcrash is not None co = _pytest._code.Code.from_function(h) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 1 - assert entry.frame.code.name == "h" + assert reprcrash.path == str(co.path) + assert reprcrash.lineno == co.firstlineno + 1 + 1 - def test_traceback_getcrashentry_empty(self): + def test_getreprcrash_empty(self): def g(): __tracebackhide__ = True raise ValueError @@ -309,12 +327,7 @@ class TestTraceback_f_g_h: g() excinfo = pytest.raises(ValueError, f) - tb = excinfo.traceback - entry = tb.getcrashentry() - co = _pytest._code.Code.from_function(g) - assert entry.frame.code.path == co.path - assert entry.lineno == co.firstlineno + 2 - assert entry.frame.code.name == "g" + assert excinfo._getreprcrash() is None def test_excinfo_exconly(): @@ -361,12 +374,15 @@ def test_excinfo_no_sourcecode(): except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() s = str(excinfo.traceback[-1]) - assert s == " File '<string>':1 in <module>\n ???\n" + # TODO: Since Python 3.13b1 under pytest-xdist, the * is `import + # sys;exec(eval(sys.stdin.readline()))` (execnet bootstrap code) + # instead of `???` like before. Is this OK? + fnmatch.fnmatch(s, " File '<string>':1 in <module>\n *\n") def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: # XXX: simplified locally testable version - tmp_path.joinpath("test.txt").write_text("{{ h()}}:") + tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8") jinja2 = pytest.importorskip("jinja2") loader = jinja2.FileSystemLoader(str(tmp_path)) @@ -375,7 +391,7 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: excinfo = pytest.raises(ValueError, template.render, h=h) for item in excinfo.traceback: print(item) # XXX: for some reason jinja.Template.render is printed in full - item.source # shouldn't fail + _ = item.source # shouldn't fail if isinstance(item.path, Path) and item.path.name == "test.txt": assert str(item.source) == "{{ h()}}:" @@ -406,7 +422,7 @@ def test_codepath_Queue_example() -> None: def test_match_succeeds(): with pytest.raises(ZeroDivisionError) as excinfo: - 0 // 0 + _ = 0 // 0 excinfo.match(r".*zero.*") @@ -420,18 +436,106 @@ def test_match_raises_error(pytester: Pytester) -> None: excinfo.match(r'[123]+') """ ) - result = pytester.runpytest() + result = pytester.runpytest("--tb=short") assert result.ret != 0 - exc_msg = "Regex pattern '[[]123[]]+' does not match 'division by zero'." - result.stdout.fnmatch_lines([f"E * AssertionError: {exc_msg}"]) + match = [ + r"E .* AssertionError: Regex pattern did not match.", + r"E .* Regex: '\[123\]\+'", + r"E .* Input: 'division by zero'", + ] + result.stdout.re_match_lines(match) result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") result = pytester.runpytest("--fulltrace") assert result.ret != 0 - result.stdout.fnmatch_lines( - ["*__tracebackhide__ = True*", f"E * AssertionError: {exc_msg}"] - ) + result.stdout.re_match_lines([r".*__tracebackhide__ = True.*", *match]) + + +class TestGroupContains: + def test_contains_exception_type(self) -> None: + exc_group = ExceptionGroup("", [RuntimeError()]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError) + + def test_doesnt_contain_exception_type(self) -> None: + exc_group = ExceptionGroup("", [ValueError()]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert not exc_info.group_contains(RuntimeError) + + def test_contains_exception_match(self) -> None: + exc_group = ExceptionGroup("", [RuntimeError("exception message")]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError, match=r"^exception message$") + + def test_doesnt_contain_exception_match(self) -> None: + exc_group = ExceptionGroup("", [RuntimeError("message that will not match")]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert not exc_info.group_contains(RuntimeError, match=r"^exception message$") + + def test_contains_exception_type_unlimited_depth(self) -> None: + exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError) + + def test_contains_exception_type_at_depth_1(self) -> None: + exc_group = ExceptionGroup("", [RuntimeError()]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError, depth=1) + + def test_doesnt_contain_exception_type_past_depth(self) -> None: + exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert not exc_info.group_contains(RuntimeError, depth=1) + + def test_contains_exception_type_specific_depth(self) -> None: + exc_group = ExceptionGroup("", [ExceptionGroup("", [RuntimeError()])]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError, depth=2) + + def test_contains_exception_match_unlimited_depth(self) -> None: + exc_group = ExceptionGroup( + "", [ExceptionGroup("", [RuntimeError("exception message")])] + ) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains(RuntimeError, match=r"^exception message$") + + def test_contains_exception_match_at_depth_1(self) -> None: + exc_group = ExceptionGroup("", [RuntimeError("exception message")]) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains( + RuntimeError, match=r"^exception message$", depth=1 + ) + + def test_doesnt_contain_exception_match_past_depth(self) -> None: + exc_group = ExceptionGroup( + "", [ExceptionGroup("", [RuntimeError("exception message")])] + ) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert not exc_info.group_contains( + RuntimeError, match=r"^exception message$", depth=1 + ) + + def test_contains_exception_match_specific_depth(self) -> None: + exc_group = ExceptionGroup( + "", [ExceptionGroup("", [RuntimeError("exception message")])] + ) + with pytest.raises(ExceptionGroup) as exc_info: + raise exc_group + assert exc_info.group_contains( + RuntimeError, match=r"^exception message$", depth=2 + ) class TestFormattedExcinfo: @@ -441,9 +545,11 @@ class TestFormattedExcinfo: source = textwrap.dedent(source) modpath = tmp_path.joinpath("mod.py") tmp_path.joinpath("__init__.py").touch() - modpath.write_text(source) + modpath.write_text(source, encoding="utf-8") importlib.invalidate_caches() - return import_path(modpath, root=tmp_path) + return import_path( + modpath, root=tmp_path, consider_namespace_packages=False + ) return importasmod @@ -461,12 +567,30 @@ class TestFormattedExcinfo: assert lines[0] == "| def f(x):" assert lines[1] == " pass" + def test_repr_source_out_of_bounds(self): + pr = FormattedExcinfo() + source = _pytest._code.Source( + """\ + def f(x): + pass + """ + ).strip() + pr.flow_marker = "|" # type: ignore[misc] + + lines = pr.get_source(source, 100) + assert len(lines) == 1 + assert lines[0] == "| ???" + + lines = pr.get_source(source, -100) + assert len(lines) == 1 + assert lines[0] == "| ???" + def test_repr_source_excinfo(self) -> None: """Check if indentation is right.""" try: def f(): - 1 / 0 + _ = 1 / 0 f() @@ -483,7 +607,7 @@ class TestFormattedExcinfo: print(line) assert lines == [ " def f():", - "> 1 / 0", + "> _ = 1 / 0", "E ZeroDivisionError: division by zero", ] @@ -520,7 +644,7 @@ raise ValueError() pr = FormattedExcinfo() try: - 1 / 0 + _ = 1 / 0 except ZeroDivisionError: excinfo = ExceptionInfo.from_current() @@ -596,7 +720,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) p = FormattedExcinfo() reprtb = p.repr_traceback_entry(excinfo.traceback[-1]) @@ -629,7 +753,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) @@ -656,7 +780,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d") - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) entry = excinfo.traceback[-1] p = FormattedExcinfo(funcargs=True) reprfuncargs = p.repr_args(entry) @@ -737,7 +861,11 @@ raise ValueError() reprtb = p.repr_traceback(excinfo) assert len(reprtb.reprentries) == 3 - def test_traceback_short_no_source(self, importasmod, monkeypatch) -> None: + def test_traceback_short_no_source( + self, + importasmod, + monkeypatch: pytest.MonkeyPatch, + ) -> None: mod = importasmod( """ def func1(): @@ -749,14 +877,14 @@ raise ValueError() excinfo = pytest.raises(ValueError, mod.entry) from _pytest._code.code import Code - monkeypatch.setattr(Code, "path", "bogus") - p = FormattedExcinfo(style="short") - reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) - lines = reprtb.lines - last_p = FormattedExcinfo(style="short") - last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo) - last_lines = last_reprtb.lines - monkeypatch.undo() + with monkeypatch.context() as mp: + mp.setattr(Code, "path", "bogus") + p = FormattedExcinfo(style="short") + reprtb = p.repr_traceback_entry(excinfo.traceback[-2]) + lines = reprtb.lines + last_p = FormattedExcinfo(style="short") + last_reprtb = last_p.repr_traceback_entry(excinfo.traceback[-1], excinfo) + last_lines = last_reprtb.lines assert lines[0] == " func1()" assert last_lines[0] == ' raise ValueError("hello")' @@ -773,7 +901,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - styles: Tuple[_TracebackStyle, ...] = ("long", "short") + styles: tuple[_TracebackStyle, ...] = ("long", "short") for style in styles: p = FormattedExcinfo(style=style) reprtb = p.repr_traceback(excinfo) @@ -900,7 +1028,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - styles: Tuple[_TracebackStyle, ...] = ("short", "long", "no") + styles: tuple[_TracebackStyle, ...] = ("short", "long", "no") for style in styles: for showlocals in (True, False): repr = excinfo.getrepr(style=style, showlocals=showlocals) @@ -930,7 +1058,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -964,7 +1092,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.f) tmp_path.joinpath("mod.py").unlink() - excinfo.traceback = excinfo.traceback.filter() + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -995,8 +1123,8 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tmp_path.joinpath("mod.py").write_text("asdf") - excinfo.traceback = excinfo.traceback.filter() + tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8") + excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) assert tw_mock.lines[0] == "" @@ -1052,9 +1180,7 @@ raise ValueError() "funcargs": funcargs, "tbfilter": tbfilter, }, - id="style={},showlocals={},funcargs={},tbfilter={}".format( - style, showlocals, funcargs, tbfilter - ), + id=f"style={style},showlocals={showlocals},funcargs={funcargs},tbfilter={tbfilter}", ) for style in ["long", "short", "line", "no", "native", "value", "auto"] for showlocals in (True, False) @@ -1062,7 +1188,7 @@ raise ValueError() for funcargs in (True, False) ], ) - def test_format_excinfo(self, reproptions: Dict[str, Any]) -> None: + def test_format_excinfo(self, reproptions: dict[str, Any]) -> None: def bar(): assert False, "some error" @@ -1093,9 +1219,11 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - excinfo.traceback = excinfo.traceback.filter() - excinfo.traceback[1].set_repr_style("short") - excinfo.traceback[2].set_repr_style("short") + excinfo.traceback = excinfo.traceback.filter(excinfo) + excinfo.traceback = _pytest._code.Traceback( + entry if i not in (1, 2) else entry.with_repr_style("short") + for i, entry in enumerate(excinfo.traceback) + ) r = excinfo.getrepr(style="long") r.toterminal(tw_mock) for line in tw_mock.lines: @@ -1216,7 +1344,7 @@ raise ValueError() """ raise_suffix = " from None" if mode == "from_none" else "" mod = importasmod( - """ + f""" def f(): try: g() @@ -1224,9 +1352,7 @@ raise ValueError() raise AttributeError(){raise_suffix} def g(): raise ValueError() - """.format( - raise_suffix=raise_suffix - ) + """ ) excinfo = pytest.raises(AttributeError, mod.f) r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress") @@ -1238,9 +1364,7 @@ raise ValueError() assert tw_mock.lines[2] == " try:" assert tw_mock.lines[3] == " g()" assert tw_mock.lines[4] == " except Exception:" - assert tw_mock.lines[5] == "> raise AttributeError(){}".format( - raise_suffix - ) + assert tw_mock.lines[5] == f"> raise AttributeError(){raise_suffix}" assert tw_mock.lines[6] == "E AttributeError" assert tw_mock.lines[7] == "" line = tw_mock.get_write_msg(8) @@ -1271,7 +1395,7 @@ raise ValueError() """ exc_handling_code = " from e" if reason == "cause" else "" mod = importasmod( - """ + f""" def f(): try: g() @@ -1279,9 +1403,7 @@ raise ValueError() raise RuntimeError('runtime problem'){exc_handling_code} def g(): raise ValueError('invalid value') - """.format( - exc_handling_code=exc_handling_code - ) + """ ) with pytest.raises(RuntimeError) as excinfo: @@ -1361,14 +1483,14 @@ raise ValueError() with pytest.raises(TypeError) as excinfo: mod.f() # previously crashed with `AttributeError: list has no attribute get` - excinfo.traceback.filter() + excinfo.traceback.filter(excinfo) @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): if encoding is None: - msg: Union[str, bytes] = "☹" + msg: str | bytes = "☹" else: msg = "☹".encode(encoding) try: @@ -1397,7 +1519,7 @@ def test_cwd_deleted(pytester: Pytester) -> None: result.stderr.no_fnmatch_line("*INTERNALERROR*") -def test_regression_nagative_line_index(pytester: Pytester) -> None: +def test_regression_negative_line_index(pytester: Pytester) -> None: """ With Python 3.10 alphas, there was an INTERNALERROR reported in https://github.com/pytest-dev/pytest/pull/8227 @@ -1466,5 +1588,203 @@ def test_no_recursion_index_on_recursion_error(): return getattr(self, "_" + attr) with pytest.raises(RuntimeError) as excinfo: - RecursionDepthError().trigger + _ = RecursionDepthError().trigger assert "maximum recursion" in str(excinfo.getrepr()) + + +def _exceptiongroup_common( + pytester: Pytester, + outer_chain: str, + inner_chain: str, + native: bool, +) -> None: + pre_raise = "exceptiongroup." if not native else "" + pre_catch = pre_raise if sys.version_info < (3, 11) else "" + filestr = f""" + {"import exceptiongroup" if not native else ""} + import pytest + + def f(): raise ValueError("From f()") + def g(): raise BaseException("From g()") + + def inner(inner_chain): + excs = [] + for callback in [f, g]: + try: + callback() + except BaseException as err: + excs.append(err) + if excs: + if inner_chain == "none": + raise {pre_raise}BaseExceptionGroup("Oops", excs) + try: + raise SyntaxError() + except SyntaxError as e: + if inner_chain == "from": + raise {pre_raise}BaseExceptionGroup("Oops", excs) from e + else: + raise {pre_raise}BaseExceptionGroup("Oops", excs) + + def outer(outer_chain, inner_chain): + try: + inner(inner_chain) + except {pre_catch}BaseExceptionGroup as e: + if outer_chain == "none": + raise + if outer_chain == "from": + raise IndexError() from e + else: + raise IndexError() + + + def test(): + outer("{outer_chain}", "{inner_chain}") + """ + pytester.makepyfile(test_excgroup=filestr) + result = pytester.runpytest() + match_lines = [] + if inner_chain in ("another", "from"): + match_lines.append(r"SyntaxError: <no detail available>") + + match_lines += [ + r" + Exception Group Traceback (most recent call last):", + rf" \| {pre_catch}BaseExceptionGroup: Oops \(2 sub-exceptions\)", + r" \| ValueError: From f\(\)", + r" \| BaseException: From g\(\)", + r"=* short test summary info =*", + ] + if outer_chain in ("another", "from"): + match_lines.append(r"FAILED test_excgroup.py::test - IndexError") + else: + match_lines.append( + rf"FAILED test_excgroup.py::test - {pre_catch}BaseExceptionGroup: Oops \(2.*" + ) + result.stdout.re_match_lines(match_lines) + + +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Native ExceptionGroup not implemented" +) +@pytest.mark.parametrize("outer_chain", ["none", "from", "another"]) +@pytest.mark.parametrize("inner_chain", ["none", "from", "another"]) +def test_native_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: + _exceptiongroup_common(pytester, outer_chain, inner_chain, native=True) + + +@pytest.mark.parametrize("outer_chain", ["none", "from", "another"]) +@pytest.mark.parametrize("inner_chain", ["none", "from", "another"]) +def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: + # with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it + pytest.importorskip("exceptiongroup") + _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False) + + +@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native")) +def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None: + """Regression test for #10903.""" + pytester.makepyfile( + """ + def test(): + __tracebackhide__ = True + 1 / 0 + """ + ) + result = pytester.runpytest("--tb", tbstyle) + assert result.ret == 1 + if tbstyle != "line": + result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"]) + if tbstyle not in ("line", "native"): + result.stdout.fnmatch_lines(["All traceback entries are hidden.*"]) + + +def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None: + """Hidden entries of chained exceptions are not shown (#1904).""" + p = pytester.makepyfile( + """ + def g1(): + __tracebackhide__ = True + str.does_not_exist + + def f3(): + __tracebackhide__ = True + 1 / 0 + + def f2(): + try: + f3() + except Exception: + g1() + + def f1(): + __tracebackhide__ = True + f2() + + def test(): + f1() + """ + ) + result = pytester.runpytest(str(p), "--tb=short") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*.py:11: in f2", + " f3()", + "E ZeroDivisionError: division by zero", + "", + "During handling of the above exception, another exception occurred:", + "*.py:20: in test", + " f1()", + "*.py:13: in f2", + " g1()", + "E AttributeError:*'does_not_exist'", + ], + consecutive=True, + ) + + +def add_note(err: BaseException, msg: str) -> None: + """Adds a note to an exception inplace.""" + if sys.version_info < (3, 11): + err.__notes__ = [*getattr(err, "__notes__", []), msg] # type: ignore[attr-defined] + else: + err.add_note(msg) + + +@pytest.mark.parametrize( + "error,notes,match", + [ + (Exception("test"), [], "test"), + (AssertionError("foo"), ["bar"], "bar"), + (AssertionError("foo"), ["bar", "baz"], "bar"), + (AssertionError("foo"), ["bar", "baz"], "baz"), + (ValueError("foo"), ["bar", "baz"], re.compile(r"bar\nbaz", re.MULTILINE)), + (ValueError("foo"), ["bar", "baz"], re.compile(r"BAZ", re.IGNORECASE)), + ], +) +def test_check_error_notes_success( + error: Exception, notes: list[str], match: str +) -> None: + for note in notes: + add_note(error, note) + + with pytest.raises(Exception, match=match): + raise error + + +@pytest.mark.parametrize( + "error, notes, match", + [ + (Exception("test"), [], "foo"), + (AssertionError("foo"), ["bar"], "baz"), + (AssertionError("foo"), ["bar"], "foo\nbaz"), + ], +) +def test_check_error_notes_failure( + error: Exception, notes: list[str], match: str +) -> None: + for note in notes: + add_note(error, note) + + with pytest.raises(AssertionError): + with pytest.raises(type(error), match=match): + raise error diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_source.py b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_source.py index 9f7be5e2458..a00259976c4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/code/test_source.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/code/test_source.py @@ -1,16 +1,14 @@ +# mypy: allow-untyped-defs # flake8: noqa # disable flake check on this file because some constructs are strange # or redundant on purpose and can't be disable on a line-by-line basis -import ast import inspect import linecache import sys import textwrap from pathlib import Path -from types import CodeType from typing import Any from typing import Dict -from typing import Optional import pytest from _pytest._code import Code @@ -257,7 +255,7 @@ def test_getfuncsource_dynamic() -> None: assert str(g_source).strip() == "def g():\n pass # pragma: no cover" -def test_getfuncsource_with_multine_string() -> None: +def test_getfuncsource_with_multiline_string() -> None: def f(): c = """while True: pass @@ -297,8 +295,8 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) - """ ) path = tmp_path.joinpath("a.py") - path.write_text(str(source)) - mod: Any = import_path(path, root=tmp_path) + path.write_text(str(source), encoding="utf-8") + mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False) s2 = Source(mod.A) assert str(source).strip() == str(s2).strip() @@ -332,8 +330,7 @@ def test_findsource(monkeypatch) -> None: lines = ["if 1:\n", " def x():\n", " pass\n"] co = compile("".join(lines), filename, "exec") - # Type ignored because linecache.cache is private. - monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) # type: ignore[attr-defined] + monkeypatch.setitem(linecache.cache, filename, (1, None, lines, filename)) src, lineno = findsource(co) assert src is not None @@ -373,7 +370,11 @@ def test_getfslineno() -> None: pass B.__name__ = B.__qualname__ = "B2" - assert getfslineno(B)[1] == -1 + # Since Python 3.13 this started working. + if sys.version_info >= (3, 13): + assert getfslineno(B)[1] != -1 + else: + assert getfslineno(B)[1] == -1 def test_code_of_object_instance_with_call() -> None: @@ -443,14 +444,9 @@ comment 4 ''' for line in range(2, 6): assert str(getstatement(line, source)) == " x = 1" - if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - tqs_start = 8 - else: - tqs_start = 10 - assert str(getstatement(10, source)) == '"""' - for line in range(6, tqs_start): + for line in range(6, 8): assert str(getstatement(line, source)) == " assert False" - for line in range(tqs_start, 10): + for line in range(8, 10): assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/conftest.py index 107aad86b25..b7e2d6111af 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/conftest.py @@ -1,10 +1,14 @@ +# mypy: allow-untyped-defs +import dataclasses import re import sys +from typing import Generator from typing import List -import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest + if sys.gettrace(): @@ -20,11 +24,31 @@ if sys.gettrace(): sys.settrace(orig_trace) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection_modifyitems(items): +@pytest.fixture(autouse=True) +def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Force terminal width to 80: some tests check the formatting of --help, which is sensible + to terminal width. + """ + monkeypatch.setenv("COLUMNS", "80") + + +@pytest.fixture(autouse=True) +def reset_colors(monkeypatch: pytest.MonkeyPatch) -> None: + """ + Reset all color-related variables to prevent them from affecting internal pytest output + in tests that depend on it. + """ + monkeypatch.delenv("PY_COLORS", raising=False) + monkeypatch.delenv("NO_COLOR", raising=False) + monkeypatch.delenv("FORCE_COLOR", raising=False) + + +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection_modifyitems(items) -> Generator[None, None, None]: """Prefer faster tests. - Use a hookwrapper to do this in the beginning, so e.g. --ff still works + Use a hook wrapper to do this in the beginning, so e.g. --ff still works correctly. """ fast_items = [] @@ -61,7 +85,7 @@ def pytest_collection_modifyitems(items): items[:] = fast_items + neutral_items + slow_items + slowest_items - yield + return (yield) @pytest.fixture @@ -104,7 +128,7 @@ def tw_mock(): @pytest.fixture -def dummy_yaml_custom_test(pytester: Pytester): +def dummy_yaml_custom_test(pytester: Pytester) -> None: """Writes a conftest file that collects and executes a dummy yaml test. Taken from the docs, but stripped down to the bare minimum, useful for @@ -149,6 +173,9 @@ def color_mapping(): "red": "\x1b[31m", "green": "\x1b[32m", "yellow": "\x1b[33m", + "light-gray": "\x1b[90m", + "light-red": "\x1b[91m", + "light-green": "\x1b[92m", "bold": "\x1b[1m", "reset": "\x1b[0m", "kw": "\x1b[94m", @@ -157,8 +184,10 @@ def color_mapping(): "number": "\x1b[94m", "str": "\x1b[33m", "print": "\x1b[96m", + "endline": "\x1b[90m\x1b[39;49;00m", } RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} + NO_COLORS = {k: "" for k in COLORS.keys()} @classmethod def format(cls, lines: List[str]) -> List[str]: @@ -175,6 +204,11 @@ def color_mapping(): """Replace color names for use with LineMatcher.re_match_lines""" return [line.format(**cls.RE_COLORS) for line in lines] + @classmethod + def strip_colors(cls, lines: List[str]) -> List[str]: + """Entirely remove every color code""" + return [line.format(**cls.NO_COLORS) for line in lines] + return ColorMapping @@ -191,20 +225,18 @@ def mock_timing(monkeypatch: MonkeyPatch): Time is static, and only advances through `sleep` calls, thus tests might sleep over large numbers and obtain accurate time() calls at the end, making tests reliable and instant. """ - import attr - @attr.s + @dataclasses.dataclass class MockTiming: + _current_time: float = 1590150050.0 - _current_time = attr.ib(default=1590150050.0) - - def sleep(self, seconds): + def sleep(self, seconds: float) -> None: self._current_time += seconds - def time(self): + def time(self) -> float: return self._current_time - def patch(self): + def patch(self) -> None: from _pytest import timing monkeypatch.setattr(timing, "sleep", self.sleep) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/deprecated_test.py b/tests/wpt/tests/tools/third_party/pytest/testing/deprecated_test.py index 9ac7fe1cacb..9e83a49d554 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/deprecated_test.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/deprecated_test.py @@ -1,23 +1,15 @@ +# mypy: allow-untyped-defs +from pathlib import Path import re import sys -import warnings -from pathlib import Path -from unittest import mock -import pytest from _pytest import deprecated from _pytest.compat import legacy_path from _pytest.pytester import Pytester +import pytest from pytest import PytestDeprecationWarning -@pytest.mark.parametrize("attribute", pytest.collect.__all__) # type: ignore -# false positive due to dynamic attribute -def test_pytest_collect_module_deprecated(attribute) -> None: - with pytest.warns(DeprecationWarning, match=attribute): - getattr(pytest.collect, attribute) - - @pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS)) @pytest.mark.filterwarnings("default") def test_external_plugins_integrated(pytester: Pytester, plugin) -> None: @@ -28,96 +20,52 @@ def test_external_plugins_integrated(pytester: Pytester, plugin) -> None: pytester.parseconfig("-p", plugin) -def test_fillfuncargs_is_deprecated() -> None: - with pytest.warns( - pytest.PytestDeprecationWarning, - match=re.escape( - "pytest._fillfuncargs() is deprecated, use " - "function._request._fillfixtures() instead if you cannot avoid reaching into internals." - ), - ): - pytest._fillfuncargs(mock.Mock()) - - -def test_fillfixtures_is_deprecated() -> None: - import _pytest.fixtures - - with pytest.warns( - pytest.PytestDeprecationWarning, - match=re.escape( - "_pytest.fixtures.fillfixtures() is deprecated, use " - "function._request._fillfixtures() instead if you cannot avoid reaching into internals." - ), - ): - _pytest.fixtures.fillfixtures(mock.Mock()) +def test_hookspec_via_function_attributes_are_deprecated(): + from _pytest.config import PytestPluginManager + pm = PytestPluginManager() -def test_minus_k_dash_is_deprecated(pytester: Pytester) -> None: - threepass = pytester.makepyfile( - test_threepass=""" - def test_one(): assert 1 - def test_two(): assert 1 - def test_three(): assert 1 - """ - ) - result = pytester.runpytest("-k=-test_two", threepass) - result.stdout.fnmatch_lines(["*The `-k '-expr'` syntax*deprecated*"]) - - -def test_minus_k_colon_is_deprecated(pytester: Pytester) -> None: - threepass = pytester.makepyfile( - test_threepass=""" - def test_one(): assert 1 - def test_two(): assert 1 - def test_three(): assert 1 - """ - ) - result = pytester.runpytest("-k", "test_two:", threepass) - result.stdout.fnmatch_lines(["*The `-k 'expr:'` syntax*deprecated*"]) + class DeprecatedHookMarkerSpec: + def pytest_bad_hook(self): + pass + pytest_bad_hook.historic = False # type: ignore[attr-defined] -def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None: - module = pytester.getmodulecol( - """ - def test_foo(): pass - """, - withinit=True, + with pytest.warns( + PytestDeprecationWarning, + match=r"Please use the pytest\.hookspec\(historic=False\) decorator", + ) as recorder: + pm.add_hookspecs(DeprecatedHookMarkerSpec) + (record,) = recorder + assert ( + record.lineno + == DeprecatedHookMarkerSpec.pytest_bad_hook.__code__.co_firstlineno ) - assert isinstance(module, pytest.Module) - package = module.parent - assert isinstance(package, pytest.Package) + assert record.filename == __file__ - with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"): - package.gethookproxy(pytester.path) - with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"): - package.isinitpath(pytester.path) +def test_hookimpl_via_function_attributes_are_deprecated(): + from _pytest.config import PytestPluginManager - # The methods on Session are *not* deprecated. - session = module.session - with warnings.catch_warnings(record=True) as rec: - session.gethookproxy(pytester.path) - session.isinitpath(pytester.path) - assert len(rec) == 0 + pm = PytestPluginManager() + class DeprecatedMarkImplPlugin: + def pytest_runtest_call(self): + pass -def test_strict_option_is_deprecated(pytester: Pytester) -> None: - """--strict is a deprecated alias to --strict-markers (#7530).""" - pytester.makepyfile( - """ - import pytest + pytest_runtest_call.tryfirst = True # type: ignore[attr-defined] - @pytest.mark.unknown - def test_foo(): pass - """ - ) - result = pytester.runpytest("--strict") - result.stdout.fnmatch_lines( - [ - "'unknown' not found in `markers` configuration option", - "*PytestRemovedIn8Warning: The --strict option is deprecated, use --strict-markers instead.", - ] + with pytest.warns( + PytestDeprecationWarning, + match=r"Please use the pytest.hookimpl\(tryfirst=True\)", + ) as recorder: + pm.register(DeprecatedMarkImplPlugin()) + (record,) = recorder + assert ( + record.lineno + == DeprecatedMarkImplPlugin.pytest_runtest_call.__code__.co_firstlineno ) + assert record.filename == __file__ def test_yield_fixture_is_deprecated() -> None: @@ -142,23 +90,6 @@ def test_private_is_deprecated() -> None: PrivateInit(10, _ispytest=True) -def test_raising_unittest_skiptest_during_collection_is_deprecated( - pytester: Pytester, -) -> None: - pytester.makepyfile( - """ - import unittest - raise unittest.SkipTest() - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*PytestRemovedIn8Warning: Raising unittest.SkipTest*", - ] - ) - - @pytest.mark.parametrize("hooktype", ["hook", "ihook"]) def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): path = legacy_path(tmp_path) @@ -190,121 +121,99 @@ def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): ) -def test_warns_none_is_deprecated(): - with pytest.warns( - PytestDeprecationWarning, - match=re.escape( - "Passing None has been deprecated.\n" - "See https://docs.pytest.org/en/latest/how-to/capture-warnings.html" - "#additional-use-cases-of-warnings-in-tests" - " for alternatives in common use cases." - ), - ): - with pytest.warns(None): # type: ignore[call-overload] - pass - - -class TestSkipMsgArgumentDeprecated: - def test_skip_with_msg_is_deprecated(self, pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - - def test_skipping_msg(): - pytest.skip(msg="skippedmsg") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - [ - "*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, " - "use pytest.skip(reason=...) instead", - '*pytest.skip(msg="skippedmsg")*', - ] - ) - result.assert_outcomes(skipped=1, warnings=1) +def test_hookimpl_warnings_for_pathlib() -> None: + class Plugin: + def pytest_ignore_collect(self, path: object) -> None: + raise NotImplementedError() - def test_fail_with_msg_is_deprecated(self, pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest + def pytest_collect_file(self, path: object) -> None: + raise NotImplementedError() - def test_failing_msg(): - pytest.fail(msg="failedmsg") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - [ - "*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, " - "use pytest.fail(reason=...) instead", - '*pytest.fail(msg="failedmsg")', - ] - ) - result.assert_outcomes(failed=1, warnings=1) + def pytest_pycollect_makemodule(self, path: object) -> None: + raise NotImplementedError() - def test_exit_with_msg_is_deprecated(self, pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest + def pytest_report_header(self, startdir: object) -> str: + raise NotImplementedError() - def test_exit_msg(): - pytest.exit(msg="exitmsg") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - [ - "*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, " - "use pytest.exit(reason=...) instead", - ] - ) - result.assert_outcomes(warnings=1) + def pytest_report_collectionfinish(self, startdir: object) -> str: + raise NotImplementedError() - -def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None: - pytester.makeconftest( - """ - def pytest_cmdline_preparse(config, args): - ... - - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*", - "*Please use pytest_load_initial_conftests hook instead.*", - ] - ) + pm = pytest.PytestPluginManager() + with pytest.warns( + pytest.PytestRemovedIn9Warning, + match=r"py\.path\.local.* argument is deprecated", + ) as wc: + pm.register(Plugin()) + assert len(wc.list) == 5 def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: mod = pytester.getmodulecol("") + class MyFile(pytest.File): + def collect(self): + raise NotImplementedError() + with pytest.warns( pytest.PytestDeprecationWarning, - match=re.escape("The (fspath: py.path.local) argument to File is deprecated."), + match=re.escape( + "The (fspath: py.path.local) argument to MyFile is deprecated." + ), ): - pytest.File.from_parent( + MyFile.from_parent( parent=mod.parent, fspath=legacy_path("bla"), ) -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason="This deprecation can only be emitted on python>=3.7", -) -def test_importing_instance_is_deprecated(pytester: Pytester) -> None: +def test_fixture_disallow_on_marked_functions(): + """Test that applying @pytest.fixture to a marked function warns (#3364).""" with pytest.warns( - pytest.PytestDeprecationWarning, - match=re.escape("The pytest.Instance collector type is deprecated"), - ): - pytest.Instance + pytest.PytestRemovedIn9Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.fixture + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError() + + # it's only possible to get one warning here because you're already prevented + # from applying @fixture twice + # ValueError("fixture is being applied more than once to the same function") + assert len(record) == 1 + +def test_fixture_disallow_marks_on_fixtures(): + """Test that applying a mark to a fixture warns (#3364).""" with pytest.warns( - pytest.PytestDeprecationWarning, - match=re.escape("The pytest.Instance collector type is deprecated"), - ): - from _pytest.python import Instance # noqa: F401 + pytest.PytestRemovedIn9Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + @pytest.fixture + def foo(): + raise NotImplementedError() + + assert len(record) == 2 # one for each mark decorator + # should point to this file + assert all(rec.filename == __file__ for rec in record) + + +def test_fixture_disallowed_between_marks(): + """Test that applying a mark to a fixture warns (#3364).""" + with pytest.warns( + pytest.PytestRemovedIn9Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.fixture + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError() + + assert len(record) == 2 # one for each mark decorator diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py index 5b00ac90e1b..d802a7f8728 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/acceptance/fixture_mock_integration.py @@ -1,8 +1,11 @@ +# mypy: allow-untyped-defs """Reproduces issue #3774""" + from unittest import mock import pytest + config = {"mykey": "ORIGINAL"} diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py index 9cd366295e7..58c41942d1c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/__init__.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_init(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py index 8f2d73cfa4f..d88c001c2cc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_foo(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py index 973ccc0c030..bba5db8b2fd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/conftest.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def pytest_ignore_collect(collection_path): return False diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py index f174823854e..2809d0cc689 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py index e69de29bb2d..58c41942d1c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py @@ -0,0 +1,3 @@ +# mypy: allow-untyped-defs +def test_init(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py index f174823854e..d88c001c2cc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py @@ -1,2 +1,3 @@ -def test(): +# mypy: allow-untyped-defs +def test_foo(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py index 8f2d73cfa4f..d88c001c2cc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/config/collect_pytest_prefix/test_foo.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_foo(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py index 8973e4252d3..64bbeefac1d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/conftest_usageerror/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs def pytest_configure(config): import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/conftest.py new file mode 100644 index 00000000000..fe1c743a686 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/conftest.py @@ -0,0 +1,23 @@ +# mypy: allow-untyped-defs +# content of conftest.py +import json + +import pytest + + +class ManifestDirectory(pytest.Directory): + def collect(self): + manifest_path = self.path / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + ihook = self.ihook + for file in manifest["files"]: + yield from ihook.pytest_collect_file( + file_path=self.path / file, parent=self + ) + + +@pytest.hookimpl +def pytest_collect_directory(path, parent): + if path.joinpath("manifest.json").is_file(): + return ManifestDirectory.from_parent(parent=parent, path=path) + return None diff --git a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/pytest.ini index e69de29bb2d..e69de29bb2d 100644 --- a/tests/wpt/tests/tools/third_party/py/py/_vendored_packages/apipkg-2.0.0.dist-info/REQUESTED +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/pytest.ini diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/manifest.json b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/manifest.json new file mode 100644 index 00000000000..6ab6d0a5222 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/manifest.json @@ -0,0 +1,6 @@ +{ + "files": [ + "test_first.py", + "test_second.py" + ] +} diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_first.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_first.py new file mode 100644 index 00000000000..890ca3dea38 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_first.py @@ -0,0 +1,4 @@ +# mypy: allow-untyped-defs +# content of test_first.py +def test_1(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_second.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_second.py new file mode 100644 index 00000000000..42108d5da84 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_second.py @@ -0,0 +1,4 @@ +# mypy: allow-untyped-defs +# content of test_second.py +def test_2(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_third.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_third.py new file mode 100644 index 00000000000..ede0f3e6025 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/customdirectory/tests/test_third.py @@ -0,0 +1,4 @@ +# mypy: allow-untyped-defs +# content of test_third.py +def test_3(): + pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py new file mode 100644 index 00000000000..e026fe3d192 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from dataclasses import field + + +def test_dataclasses() -> None: + @dataclass + class SimpleDataObject: + field_a: int = field() + field_b: str = field() + + def __eq__(self, __o: object) -> bool: + return super().__eq__(__o) + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + assert left == right diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_initvar.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_initvar.py new file mode 100644 index 00000000000..d687fc22530 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_initvar.py @@ -0,0 +1,13 @@ +# mypy: allow-untyped-defs +from dataclasses import dataclass +from dataclasses import InitVar + + +@dataclass +class Foo: + init_only: InitVar[int] + real_attr: int + + +def test_demonstrate(): + assert Foo(1, 2) == Foo(1, 3) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 0945790f004..801aa0a732e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs from dataclasses import dataclass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py index e471d06d643..c8a124f5416 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/__main__.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_this_is_ignored(): assert True diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py index 700cc9750cf..26a4d90bc89 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/doctest/main_py/test_normal_module.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs def test_doc(): """ >>> 10 > 5 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py index a7a5e9db80a..fe1ae620aa6 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py index f174823854e..2809d0cc689 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/custom_item/foo/test_foo.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py index be5adbeb6e5..3a5d3ac33fe 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py index df36da1369b..d0c4bdbdfd9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_1(arg1): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py index 00981c5dc12..a1f3b2d58b9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py index 1c34f94acc4..45e9744786a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_2(arg2): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py index d1efcbb338c..84e5256f070 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py index 5dfd2f77957..7f1769beb9b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py index 4e22ce5a137..ad26fdd8cdc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py index 0d891fbb503..9ee74a47186 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_spam(spam): assert spam == "spamspam" diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py index 5dfd2f77957..7f1769beb9b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py index 46d1446f470..fa688f0a844 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py index 87a0c894111..f78a57c322b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py index 0661cb301fc..12e0e3e91d4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py index 256b92a17dd..8b6e8697e06 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py index e15dbd2ca45..40587cf2bd1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py index b775203231f..0cc8446d8ee 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py index 75514bf8b8c..a2ab7ee330d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_fixture_named_request.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py index 055a1220b1c..0f316f0e449 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py index cb8f5d671ea..bde5c0711ac 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/conftest.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest @@ -11,4 +12,5 @@ def pytest_collect_file(file_path, parent): class MyItem(pytest.Item): - pass + def runtest(self): + raise NotImplementedError() diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py index 56444d14748..dd18e1741f0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_hello(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py index e44367fca04..39766164490 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/issue_519.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pprint from typing import List from typing import Tuple @@ -22,13 +23,13 @@ def checked_order(): assert order == [ ("issue_519.py", "fix1", "arg1v1"), ("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"), - ("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"), ("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"), + ("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"), ("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"), ("issue_519.py", "fix1", "arg1v2"), ("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"), - ("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"), ("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"), + ("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"), ("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"), ] diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py index 35a2c7b7628..d95ad0a83d9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py index ff1eaf7d6bb..17085e50b54 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/generate_folders.py @@ -1,6 +1,8 @@ +# mypy: allow-untyped-defs import argparse import pathlib + HERE = pathlib.Path(__file__).parent TEST_CONTENT = (HERE / "template_test.py").read_bytes() diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py index 064ade190a1..f50eb65525c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/perf_examples/collect_stats/template_test.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_x(): pass diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py index 8675eb2fa62..4aa35faa0b6 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/tmpdir/tmp_path_fixture.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py index d421ce927c9..d66b66df5b7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import unittest import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py index 93f79bb3b2e..7550a097576 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class.""" + import unittest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py index 4f251dcba17..48f7e476f40 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_class.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class.""" + import unittest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py index 98befbe510f..eee4263d22b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_setup_skip_module.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """setUpModule is always called, even if all tests in the module are skipped""" + import unittest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py index 1cd2168604c..a82ddaebcc3 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs from typing import List from unittest import IsolatedAsyncioTestCase diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py index fb26617067c..e9b10171e8d 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -1,4 +1,6 @@ +# mypy: allow-untyped-defs """Issue #7110""" + import asyncio from typing import List diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py index 78dfece684e..2a4a66509a4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/unittest/test_unittest_plain_async.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import unittest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py index 6985caa4407..be64a1ff2c8 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import warnings import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py index b8c11cb71c9..95fa795efe0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import warnings import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py index 636d04a5505..5204fde8a85 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs from test_1 import func diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/create_executable.py b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/create_executable.py index 998df7b1ca7..fbfda2e5d94 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/create_executable.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/create_executable.py @@ -1,11 +1,13 @@ """Generate an executable with pytest runner embedded using PyInstaller.""" + if __name__ == "__main__": - import pytest import subprocess + import pytest + hidden = [] for x in pytest.freeze_includes(): hidden.extend(["--hidden-import", x]) hidden.extend(["--hidden-import", "distutils"]) - args = ["pyinstaller", "--noconfirm"] + hidden + ["runtests_script.py"] + args = ["pyinstaller", "--noconfirm", *hidden, "runtests_script.py"] subprocess.check_call(" ".join(args), shell=True) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/runtests_script.py b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/runtests_script.py index 591863016ac..ef63a2d15b9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/runtests_script.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/runtests_script.py @@ -5,6 +5,7 @@ pytest main(). if __name__ == "__main__": import sys + import pytest sys.exit(pytest.main()) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py index 08a55552abb..425f29a649c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tests/test_trivial.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs def test_upper(): assert "foo".upper() == "FOO" diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tox_run.py b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tox_run.py index 678a69c858a..7fd63cf1218 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tox_run.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/freeze/tox_run.py @@ -2,6 +2,7 @@ Called by tox.ini: uses the generated executable to run the tests in ./tests/ directory. """ + if __name__ == "__main__": import os import sys diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_pprint.py b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_pprint.py new file mode 100644 index 00000000000..15fe6611280 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_pprint.py @@ -0,0 +1,406 @@ +from collections import ChainMap +from collections import Counter +from collections import defaultdict +from collections import deque +from collections import OrderedDict +from dataclasses import dataclass +import textwrap +from types import MappingProxyType +from types import SimpleNamespace +from typing import Any + +from _pytest._io.pprint import PrettyPrinter +import pytest + + +@dataclass +class EmptyDataclass: + pass + + +@dataclass +class DataclassWithOneItem: + foo: str + + +@dataclass +class DataclassWithTwoItems: + foo: str + bar: str + + +@pytest.mark.parametrize( + ("data", "expected"), + ( + pytest.param( + EmptyDataclass(), + "EmptyDataclass()", + id="dataclass-empty", + ), + pytest.param( + DataclassWithOneItem(foo="bar"), + """ + DataclassWithOneItem( + foo='bar', + ) + """, + id="dataclass-one-item", + ), + pytest.param( + DataclassWithTwoItems(foo="foo", bar="bar"), + """ + DataclassWithTwoItems( + foo='foo', + bar='bar', + ) + """, + id="dataclass-two-items", + ), + pytest.param( + {}, + "{}", + id="dict-empty", + ), + pytest.param( + {"one": 1}, + """ + { + 'one': 1, + } + """, + id="dict-one-item", + ), + pytest.param( + {"one": 1, "two": 2}, + """ + { + 'one': 1, + 'two': 2, + } + """, + id="dict-two-items", + ), + pytest.param(OrderedDict(), "OrderedDict()", id="ordereddict-empty"), + pytest.param( + OrderedDict({"one": 1}), + """ + OrderedDict({ + 'one': 1, + }) + """, + id="ordereddict-one-item", + ), + pytest.param( + OrderedDict({"one": 1, "two": 2}), + """ + OrderedDict({ + 'one': 1, + 'two': 2, + }) + """, + id="ordereddict-two-items", + ), + pytest.param( + [], + "[]", + id="list-empty", + ), + pytest.param( + [1], + """ + [ + 1, + ] + """, + id="list-one-item", + ), + pytest.param( + [1, 2], + """ + [ + 1, + 2, + ] + """, + id="list-two-items", + ), + pytest.param( + tuple(), + "()", + id="tuple-empty", + ), + pytest.param( + (1,), + """ + ( + 1, + ) + """, + id="tuple-one-item", + ), + pytest.param( + (1, 2), + """ + ( + 1, + 2, + ) + """, + id="tuple-two-items", + ), + pytest.param( + set(), + "set()", + id="set-empty", + ), + pytest.param( + {1}, + """ + { + 1, + } + """, + id="set-one-item", + ), + pytest.param( + {1, 2}, + """ + { + 1, + 2, + } + """, + id="set-two-items", + ), + pytest.param( + MappingProxyType({}), + "mappingproxy({})", + id="mappingproxy-empty", + ), + pytest.param( + MappingProxyType({"one": 1}), + """ + mappingproxy({ + 'one': 1, + }) + """, + id="mappingproxy-one-item", + ), + pytest.param( + MappingProxyType({"one": 1, "two": 2}), + """ + mappingproxy({ + 'one': 1, + 'two': 2, + }) + """, + id="mappingproxy-two-items", + ), + pytest.param( + SimpleNamespace(), + "namespace()", + id="simplenamespace-empty", + ), + pytest.param( + SimpleNamespace(one=1), + """ + namespace( + one=1, + ) + """, + id="simplenamespace-one-item", + ), + pytest.param( + SimpleNamespace(one=1, two=2), + """ + namespace( + one=1, + two=2, + ) + """, + id="simplenamespace-two-items", + ), + pytest.param( + defaultdict(str), "defaultdict(<class 'str'>, {})", id="defaultdict-empty" + ), + pytest.param( + defaultdict(str, {"one": "1"}), + """ + defaultdict(<class 'str'>, { + 'one': '1', + }) + """, + id="defaultdict-one-item", + ), + pytest.param( + defaultdict(str, {"one": "1", "two": "2"}), + """ + defaultdict(<class 'str'>, { + 'one': '1', + 'two': '2', + }) + """, + id="defaultdict-two-items", + ), + pytest.param( + Counter(), + "Counter()", + id="counter-empty", + ), + pytest.param( + Counter("1"), + """ + Counter({ + '1': 1, + }) + """, + id="counter-one-item", + ), + pytest.param( + Counter("121"), + """ + Counter({ + '1': 2, + '2': 1, + }) + """, + id="counter-two-items", + ), + pytest.param(ChainMap(), "ChainMap({})", id="chainmap-empty"), + pytest.param( + ChainMap({"one": 1, "two": 2}), + """ + ChainMap( + { + 'one': 1, + 'two': 2, + }, + ) + """, + id="chainmap-one-item", + ), + pytest.param( + ChainMap({"one": 1}, {"two": 2}), + """ + ChainMap( + { + 'one': 1, + }, + { + 'two': 2, + }, + ) + """, + id="chainmap-two-items", + ), + pytest.param( + deque(), + "deque([])", + id="deque-empty", + ), + pytest.param( + deque([1]), + """ + deque([ + 1, + ]) + """, + id="deque-one-item", + ), + pytest.param( + deque([1, 2]), + """ + deque([ + 1, + 2, + ]) + """, + id="deque-two-items", + ), + pytest.param( + deque([1, 2], maxlen=3), + """ + deque(maxlen=3, [ + 1, + 2, + ]) + """, + id="deque-maxlen", + ), + pytest.param( + { + "chainmap": ChainMap({"one": 1}, {"two": 2}), + "counter": Counter("122"), + "dataclass": DataclassWithTwoItems(foo="foo", bar="bar"), + "defaultdict": defaultdict(str, {"one": "1", "two": "2"}), + "deque": deque([1, 2], maxlen=3), + "dict": {"one": 1, "two": 2}, + "list": [1, 2], + "mappingproxy": MappingProxyType({"one": 1, "two": 2}), + "ordereddict": OrderedDict({"one": 1, "two": 2}), + "set": {1, 2}, + "simplenamespace": SimpleNamespace(one=1, two=2), + "tuple": (1, 2), + }, + """ + { + 'chainmap': ChainMap( + { + 'one': 1, + }, + { + 'two': 2, + }, + ), + 'counter': Counter({ + '2': 2, + '1': 1, + }), + 'dataclass': DataclassWithTwoItems( + foo='foo', + bar='bar', + ), + 'defaultdict': defaultdict(<class 'str'>, { + 'one': '1', + 'two': '2', + }), + 'deque': deque(maxlen=3, [ + 1, + 2, + ]), + 'dict': { + 'one': 1, + 'two': 2, + }, + 'list': [ + 1, + 2, + ], + 'mappingproxy': mappingproxy({ + 'one': 1, + 'two': 2, + }), + 'ordereddict': OrderedDict({ + 'one': 1, + 'two': 2, + }), + 'set': { + 1, + 2, + }, + 'simplenamespace': namespace( + one=1, + two=2, + ), + 'tuple': ( + 1, + 2, + ), + } + """, + id="deep-example", + ), + ), +) +def test_consistent_pretty_printer(data: Any, expected: str) -> None: + assert PrettyPrinter().pformat(data) == textwrap.dedent(expected).strip() diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_saferepr.py b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_saferepr.py index 63d3af822b1..5d270f1756c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_saferepr.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_saferepr.py @@ -1,7 +1,8 @@ -import pytest -from _pytest._io.saferepr import _pformat_dispatch +# mypy: allow-untyped-defs from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited +import pytest def test_simple_repr(): @@ -58,9 +59,7 @@ def test_exceptions() -> None: obj = BrokenRepr(BrokenReprException("omg even worse")) s2 = saferepr(obj) assert s2 == ( - "<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format( - exp_exc, id(obj) - ) + f"<[unpresentable exception ({exp_exc!s}) raised in repr()] BrokenRepr object at 0x{id(obj):x}>" ) @@ -80,7 +79,7 @@ def test_baseexception(): raise self.exc_type(*args) raise self.exc_type - def __str__(self): + def __str__(self): # noqa: PLE0307 self.raise_exc("__str__") def __repr__(self): @@ -98,14 +97,12 @@ def test_baseexception(): baseexc_str = BaseException("__str__") obj = BrokenObj(RaisingOnStrRepr([BaseException])) assert saferepr(obj) == ( - "<[unpresentable exception ({!r}) " - "raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj)) + f"<[unpresentable exception ({baseexc_str!r}) " + f"raised in repr()] BrokenObj object at 0x{id(obj):x}>" ) obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])])) assert saferepr(obj) == ( - "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format( - baseexc_str, id(obj) - ) + f"<[{baseexc_str!r} raised in repr()] BrokenObj object at 0x{id(obj):x}>" ) with pytest.raises(KeyboardInterrupt): @@ -158,12 +155,6 @@ def test_unicode(): assert saferepr(val) == reprval -def test_pformat_dispatch(): - assert _pformat_dispatch("a") == "'a'" - assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'" - assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')" - - def test_broken_getattribute(): """saferepr() can create proper representations of classes with broken __getattribute__ (#7145) @@ -179,3 +170,23 @@ def test_broken_getattribute(): assert saferepr(SomeClass()).startswith( "<[RuntimeError() raised in repr()] SomeClass object at 0x" ) + + +def test_saferepr_unlimited(): + dict5 = {f"v{i}": i for i in range(5)} + assert saferepr_unlimited(dict5) == "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4}" + + dict_long = {f"v{i}": i for i in range(1_000)} + r = saferepr_unlimited(dict_long) + assert "..." not in r + assert "\n" not in r + + +def test_saferepr_unlimited_exc(): + class A: + def __repr__(self): + raise ValueError(42) + + assert saferepr_unlimited(A()).startswith( + "<[ValueError(42) raised in repr()] A object at 0x" + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py index 4866c94a558..afa8d5cae87 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_terminalwriter.py @@ -1,15 +1,17 @@ +# mypy: allow-untyped-defs import io import os +from pathlib import Path import re import shutil import sys -from pathlib import Path from typing import Generator +from typing import Optional from unittest import mock -import pytest from _pytest._io import terminalwriter from _pytest.monkeypatch import MonkeyPatch +import pytest # These tests were initially copied from py 1.8.1. @@ -56,7 +58,7 @@ def test_terminalwriter_not_unicode() -> None: file = io.TextIOWrapper(buffer, encoding="cp1252") tw = terminalwriter.TerminalWriter(file) tw.write("hello 🌀 wôrld אבג", flush=True) - assert buffer.getvalue() == br"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2" + assert buffer.getvalue() == rb"hello \U0001f300 w\xf4rld \u05d0\u05d1\u05d2" win32 = int(sys.platform == "win32") @@ -164,53 +166,67 @@ def test_attr_hasmarkup() -> None: assert "\x1b[0m" in s -def assert_color_set(): +def assert_color(expected: bool, default: Optional[bool] = None) -> None: file = io.StringIO() - tw = terminalwriter.TerminalWriter(file) - assert tw.hasmarkup + if default is None: + default = not expected + file.isatty = lambda: default # type: ignore + tw = terminalwriter.TerminalWriter(file=file) + assert tw.hasmarkup is expected tw.line("hello", bold=True) s = file.getvalue() - assert len(s) > len("hello\n") - assert "\x1b[1m" in s - assert "\x1b[0m" in s - - -def assert_color_not_set(): - f = io.StringIO() - f.isatty = lambda: True # type: ignore - tw = terminalwriter.TerminalWriter(file=f) - assert not tw.hasmarkup - tw.line("hello", bold=True) - s = f.getvalue() - assert s == "hello\n" + if expected: + assert len(s) > len("hello\n") + assert "\x1b[1m" in s + assert "\x1b[0m" in s + else: + assert s == "hello\n" def test_should_do_markup_PY_COLORS_eq_1(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "PY_COLORS", "1") - assert_color_set() + assert_color(True) def test_should_not_do_markup_PY_COLORS_eq_0(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "PY_COLORS", "0") - assert_color_not_set() + assert_color(False) def test_should_not_do_markup_NO_COLOR(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "NO_COLOR", "1") - assert_color_not_set() + assert_color(False) def test_should_do_markup_FORCE_COLOR(monkeypatch: MonkeyPatch) -> None: monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") - assert_color_set() + assert_color(True) -def test_should_not_do_markup_NO_COLOR_and_FORCE_COLOR( +@pytest.mark.parametrize( + ["NO_COLOR", "FORCE_COLOR", "expected"], + [ + ("1", "1", False), + ("", "1", True), + ("1", "", False), + ], +) +def test_NO_COLOR_and_FORCE_COLOR( monkeypatch: MonkeyPatch, + NO_COLOR: str, + FORCE_COLOR: str, + expected: bool, ) -> None: - monkeypatch.setitem(os.environ, "NO_COLOR", "1") - monkeypatch.setitem(os.environ, "FORCE_COLOR", "1") - assert_color_not_set() + monkeypatch.setitem(os.environ, "NO_COLOR", NO_COLOR) + monkeypatch.setitem(os.environ, "FORCE_COLOR", FORCE_COLOR) + assert_color(expected) + + +def test_empty_NO_COLOR_and_FORCE_COLOR_ignored(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setitem(os.environ, "NO_COLOR", "") + monkeypatch.setitem(os.environ, "FORCE_COLOR", "") + assert_color(True, True) + assert_color(False, False) class TestTerminalWriterLineWidth: @@ -254,7 +270,7 @@ class TestTerminalWriterLineWidth: pytest.param( True, True, - "{kw}assert{hl-reset} {number}0{hl-reset}\n", + "{reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n", id="with markup and code_highlight", ), pytest.param( @@ -291,3 +307,17 @@ def test_code_highlight(has_markup, code_highlight, expected, color_mapping): match=re.escape("indents size (2) should have same size as lines (1)"), ): tw._write_source(["assert 0"], [" ", " "]) + + +def test_highlight_empty_source() -> None: + """Don't crash trying to highlight empty source code. + + Issue #11758. + """ + f = io.StringIO() + tw = terminalwriter.TerminalWriter(f) + tw.hasmarkup = True + tw.code_highlight = True + tw._write_source([]) + + assert f.getvalue() == "" diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_wcwidth.py b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_wcwidth.py index 7cc74df5d07..82503b8300c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/io/test_wcwidth.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/io/test_wcwidth.py @@ -1,6 +1,6 @@ -import pytest from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcwidth +import pytest @pytest.mark.parametrize( @@ -11,11 +11,11 @@ from _pytest._io.wcwidth import wcwidth ("a", 1), ("1", 1), ("א", 1), - ("\u200B", 0), - ("\u1ABE", 0), + ("\u200b", 0), + ("\u1abe", 0), ("\u0591", 0), ("🉐", 2), - ("$", 2), + ("$", 2), # noqa: RUF001 ], ) def test_wcwidth(c: str, expected: int) -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_fixture.py b/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_fixture.py index bcb20de5805..c1cfff632af 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_fixture.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_fixture.py @@ -1,19 +1,36 @@ +# mypy: disable-error-code="attr-defined" +# mypy: disallow-untyped-defs import logging +from typing import Iterator -import pytest from _pytest.logging import caplog_records_key from _pytest.pytester import Pytester +import pytest + logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__ + ".baz") +@pytest.fixture(autouse=True) +def cleanup_disabled_logging() -> Iterator[None]: + """Simple fixture that ensures that a test doesn't disable logging. + + This is necessary because ``logging.disable()`` is global, so a test disabling logging + and not cleaning up after will break every test that runs after it. + + This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion. + """ + yield + logging.disable(logging.NOTSET) + + def test_fixture_help(pytester: Pytester) -> None: result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines(["*caplog*"]) -def test_change_level(caplog): +def test_change_level(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.debug("handler DEBUG level") logger.info("handler INFO level") @@ -28,10 +45,27 @@ def test_change_level(caplog): assert "CRITICAL" in caplog.text +def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None: + logging.disable(logging.CRITICAL) + assert logging.root.manager.disable == logging.CRITICAL + caplog.set_level(logging.WARNING) + logger.info("handler INFO level") + logger.warning("handler WARNING level") + + caplog.set_level(logging.CRITICAL, logger=sublogger.name) + sublogger.warning("logger SUB_WARNING level") + sublogger.critical("logger SUB_CRITICAL level") + + assert "INFO" not in caplog.text + assert "WARNING" in caplog.text + assert "SUB_WARNING" not in caplog.text + assert "SUB_CRITICAL" in caplog.text + + def test_change_level_undo(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test. - Tests the logging output themselves (affacted both by logger and handler levels). + Tests the logging output themselves (affected both by logger and handler levels). """ pytester.makepyfile( """ @@ -54,7 +88,36 @@ def test_change_level_undo(pytester: Pytester) -> None: result.stdout.no_fnmatch_line("*log from test2*") -def test_change_level_undos_handler_level(pytester: Pytester) -> None: +def test_change_disabled_level_undo(pytester: Pytester) -> None: + """Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test. + + Tests the logging output themselves (affected by disabled logging level). + """ + pytester.makepyfile( + """ + import logging + + def test1(caplog): + logging.disable(logging.CRITICAL) + caplog.set_level(logging.INFO) + # using + operator here so fnmatch_lines doesn't match the code in the traceback + logging.info('log from ' + 'test1') + assert 0 + + def test2(caplog): + # using + operator here so fnmatch_lines doesn't match the code in the traceback + # use logging.warning because we need a level that will show up if logging.disabled + # isn't reset to ``CRITICAL`` after test1. + logging.warning('log from ' + 'test2') + assert 0 + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) + result.stdout.no_fnmatch_line("*log from test2*") + + +def test_change_level_undoes_handler_level(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test (handler). Issue #7569. Tests the handler level specifically. @@ -82,7 +145,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None: result.assert_outcomes(passed=3) -def test_with_statement(caplog): +def test_with_statement_at_level(caplog: pytest.LogCaptureFixture) -> None: with caplog.at_level(logging.INFO): logger.debug("handler DEBUG level") logger.info("handler INFO level") @@ -97,7 +160,84 @@ def test_with_statement(caplog): assert "CRITICAL" in caplog.text -def test_log_access(caplog): +def test_with_statement_at_level_logging_disabled( + caplog: pytest.LogCaptureFixture, +) -> None: + logging.disable(logging.CRITICAL) + assert logging.root.manager.disable == logging.CRITICAL + with caplog.at_level(logging.WARNING): + logger.debug("handler DEBUG level") + logger.info("handler INFO level") + logger.warning("handler WARNING level") + logger.error("handler ERROR level") + logger.critical("handler CRITICAL level") + + assert logging.root.manager.disable == logging.INFO + + with caplog.at_level(logging.CRITICAL, logger=sublogger.name): + sublogger.warning("logger SUB_WARNING level") + sublogger.critical("logger SUB_CRITICAL level") + + assert "DEBUG" not in caplog.text + assert "INFO" not in caplog.text + assert "WARNING" in caplog.text + assert "ERROR" in caplog.text + assert " CRITICAL" in caplog.text + assert "SUB_WARNING" not in caplog.text + assert "SUB_CRITICAL" in caplog.text + assert logging.root.manager.disable == logging.CRITICAL + + +def test_with_statement_filtering(caplog: pytest.LogCaptureFixture) -> None: + class TestFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + record.msg = "filtered handler call" + return True + + with caplog.at_level(logging.INFO): + with caplog.filtering(TestFilter()): + logger.info("handler call") + logger.info("handler call") + + filtered_tuple, unfiltered_tuple = caplog.record_tuples + assert filtered_tuple == ("test_fixture", 20, "filtered handler call") + assert unfiltered_tuple == ("test_fixture", 20, "handler call") + + +@pytest.mark.parametrize( + "level_str,expected_disable_level", + [ + ("CRITICAL", logging.ERROR), + ("ERROR", logging.WARNING), + ("WARNING", logging.INFO), + ("INFO", logging.DEBUG), + ("DEBUG", logging.NOTSET), + ("NOTSET", logging.NOTSET), + ("NOTVALIDLEVEL", logging.NOTSET), + ], +) +def test_force_enable_logging_level_string( + caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int +) -> None: + """Test _force_enable_logging using a level string. + + ``expected_disable_level`` is one level below ``level_str`` because the disabled log level + always needs to be *at least* one level lower than the level that caplog is trying to capture. + """ + test_logger = logging.getLogger("test_str_level_force_enable") + # Emulate a testing environment where all logging is disabled. + logging.disable(logging.CRITICAL) + # Make sure all logging is disabled. + assert not test_logger.isEnabledFor(logging.CRITICAL) + # Un-disable logging for `level_str`. + caplog._force_enable_logging(level_str, test_logger) + # Make sure that the disabled level is now one below the requested logging level. + # We don't use `isEnabledFor` here because that also checks the level set by + # `logging.setLevel()` which is irrelevant to `logging.disable()`. + assert test_logger.manager.disable == expected_disable_level + + +def test_log_access(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.info("boo %s", "arg") assert caplog.records[0].levelname == "INFO" @@ -105,7 +245,7 @@ def test_log_access(caplog): assert "boo arg" in caplog.text -def test_messages(caplog): +def test_messages(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.info("boo %s", "arg") logger.info("bar %s\nbaz %s", "arg1", "arg2") @@ -126,14 +266,14 @@ def test_messages(caplog): assert "Exception" not in caplog.messages[-1] -def test_record_tuples(caplog): +def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.info("boo %s", "arg") assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")] -def test_unicode(caplog): +def test_unicode(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.info("bū") assert caplog.records[0].levelname == "INFO" @@ -141,7 +281,7 @@ def test_unicode(caplog): assert "bū" in caplog.text -def test_clear(caplog): +def test_clear(caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) logger.info("bū") assert len(caplog.records) @@ -152,7 +292,9 @@ def test_clear(caplog): @pytest.fixture -def logging_during_setup_and_teardown(caplog): +def logging_during_setup_and_teardown( + caplog: pytest.LogCaptureFixture, +) -> Iterator[None]: caplog.set_level("INFO") logger.info("a_setup_log") yield @@ -160,7 +302,17 @@ def logging_during_setup_and_teardown(caplog): assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"] -def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown): +def private_assert_caplog_records_is_setup_call( + caplog: pytest.LogCaptureFixture, +) -> None: + # This reaches into private API, don't use this type of thing in real tests! + caplog_records = caplog._item.stash[caplog_records_key] + assert set(caplog_records) == {"setup", "call"} + + +def test_captures_for_all_stages( + caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None +) -> None: assert not caplog.records assert not caplog.get_records("call") logger.info("a_call_log") @@ -168,8 +320,27 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] - # This reaches into private API, don't use this type of thing in real tests! - assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"} + private_assert_caplog_records_is_setup_call(caplog) + + +def test_clear_for_call_stage( + caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None +) -> None: + logger.info("a_call_log") + assert [x.message for x in caplog.get_records("call")] == ["a_call_log"] + assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] + private_assert_caplog_records_is_setup_call(caplog) + + caplog.clear() + + assert caplog.get_records("call") == [] + assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] + private_assert_caplog_records_is_setup_call(caplog) + + logging.info("a_call_log_after_clear") + assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"] + assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] + private_assert_caplog_records_is_setup_call(caplog) def test_ini_controls_global_log_level(pytester: Pytester) -> None: @@ -195,11 +366,11 @@ def test_ini_controls_global_log_level(pytester: Pytester) -> None: ) result = pytester.runpytest() - # make sure that that we get a '0' exit code for the testsuite + # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 -def test_caplog_can_override_global_log_level(pytester: Pytester) -> None: +def test_can_override_global_log_level(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -238,7 +409,7 @@ def test_caplog_can_override_global_log_level(pytester: Pytester) -> None: assert result.ret == 0 -def test_caplog_captures_despite_exception(pytester: Pytester) -> None: +def test_captures_despite_exception(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_reporting.py b/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_reporting.py index 323ff7b2446..7e592febf56 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_reporting.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/logging/test_reporting.py @@ -1,14 +1,15 @@ +# mypy: allow-untyped-defs import io import os import re from typing import cast -import pytest from _pytest.capture import CaptureManager from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.pytester import Pytester from _pytest.terminal import TerminalReporter +import pytest def test_nothing_logged(pytester: Pytester) -> None: @@ -77,14 +78,14 @@ def test_root_logger_affected(pytester: Pytester) -> None: assert "warning text going to logger" not in stdout assert "info text going to logger" not in stdout - # The log file should contain the warning and the error log messages and - # not the info one, because the default level of the root logger is - # WARNING. + # The log file should only contain the error log messages and + # not the warning or info ones, because the root logger is set to + # ERROR using --log-level=ERROR. assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "info text going to logger" not in contents - assert "warning text going to logger" in contents + assert "warning text going to logger" not in contents assert "error text going to logger" in contents @@ -176,13 +177,11 @@ def test_teardown_logging(pytester: Pytester) -> None: def test_log_cli_enabled_disabled(pytester: Pytester, enabled: bool) -> None: msg = "critical message logged by test" pytester.makepyfile( - """ + f""" import logging def test_log_cli(): - logging.critical("{}") - """.format( - msg - ) + logging.critical("{msg}") + """ ) if enabled: pytester.makeini( @@ -656,12 +655,79 @@ def test_log_file_cli(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents + + +def test_log_file_mode_cli(pytester: Pytester) -> None: + # Default log file level + pytester.makepyfile( + """ + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_file_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + print('PASSED') + """ + ) + + log_file = str(pytester.path.joinpath("pytest.log")) + + with open(log_file, mode="w", encoding="utf-8") as wfh: + wfh.write("A custom header\n") + + result = pytester.runpytest( + "-s", + f"--log-file={log_file}", + "--log-file-mode=a", + "--log-file-level=WARNING", + ) + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(["test_log_file_mode_cli.py PASSED"]) + + # make sure that we get a '0' exit code for the testsuite + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() + assert "A custom header" in contents assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents +def test_log_file_mode_cli_invalid(pytester: Pytester) -> None: + # Default log file level + pytester.makepyfile( + """ + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_file_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + """ + ) + + log_file = str(pytester.path.joinpath("pytest.log")) + + result = pytester.runpytest( + "-s", + f"--log-file={log_file}", + "--log-file-mode=b", + "--log-file-level=WARNING", + ) + + # make sure that we get a '4' exit code for the testsuite + assert result.ret == ExitCode.USAGE_ERROR + + def test_log_file_cli_level(pytester: Pytester) -> None: # Default log file level pytester.makepyfile( @@ -687,7 +753,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -709,13 +775,11 @@ def test_log_file_ini(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( - """ + f""" [pytest] - log_file={} + log_file={log_file} log_file_level=WARNING - """.format( - log_file - ) + """ ) pytester.makepyfile( """ @@ -738,23 +802,62 @@ def test_log_file_ini(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents -def test_log_file_ini_level(pytester: Pytester) -> None: +def test_log_file_mode_ini(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( + f""" + [pytest] + log_file={log_file} + log_file_mode=a + log_file_level=WARNING """ + ) + pytester.makepyfile( + """ + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('logging-plugin') + assert plugin.log_file_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + print('PASSED') + """ + ) + + with open(log_file, mode="w", encoding="utf-8") as wfh: + wfh.write("A custom header\n") + + result = pytester.runpytest("-s") + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines(["test_log_file_mode_ini.py PASSED"]) + + assert result.ret == ExitCode.OK + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert "A custom header" in contents + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents + + +def test_log_file_ini_level(pytester: Pytester) -> None: + log_file = str(pytester.path.joinpath("pytest.log")) + + pytester.makeini( + f""" [pytest] - log_file={} + log_file={log_file} log_file_level = INFO - """.format( - log_file - ) + """ ) pytester.makepyfile( """ @@ -777,7 +880,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -787,13 +890,11 @@ def test_log_file_unicode(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( - """ + f""" [pytest] - log_file={} + log_file={log_file} log_file_level = INFO - """.format( - log_file - ) + """ ) pytester.makepyfile( """\ @@ -830,9 +931,10 @@ def test_live_logging_suspends_capture( We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin is installed. """ - import logging import contextlib from functools import partial + import logging + from _pytest.logging import _LiveLoggingStreamHandler class MockCaptureManager: @@ -922,13 +1024,11 @@ def test_collection_logging_to_file(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( - """ + f""" [pytest] - log_file={} + log_file={log_file} log_file_level = INFO - """.format( - log_file - ) + """ ) pytester.makepyfile( @@ -960,14 +1060,12 @@ def test_log_in_hooks(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( - """ + f""" [pytest] - log_file={} + log_file={log_file} log_file_level = INFO log_cli=true - """.format( - log_file - ) + """ ) pytester.makeconftest( """ @@ -985,7 +1083,7 @@ def test_log_in_hooks(pytester: Pytester) -> None: ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"]) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "sessionstart" in contents assert "runtestloop" in contents @@ -996,14 +1094,12 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None: log_file = str(pytester.path.joinpath("pytest.log")) pytester.makeini( - """ + f""" [pytest] - log_file={} + log_file={log_file} log_file_level = INFO log_cli=true - """.format( - log_file - ) + """ ) pytester.makeconftest( """ @@ -1021,7 +1117,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None: """ ) pytester.runpytest() - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert contents.count("logreport") == 3 @@ -1037,19 +1133,17 @@ def test_log_set_path(pytester: Pytester) -> None: """ ) pytester.makeconftest( - """ + f""" import os import pytest - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup(item): config = item.config logging_plugin = config.pluginmanager.get_plugin("logging-plugin") - report_file = os.path.join({}, item._request.node.name) + report_file = os.path.join({report_dir_base!r}, item._request.node.name) logging_plugin.set_log_path(report_file) - yield - """.format( - repr(report_dir_base) - ) + return (yield) + """ ) pytester.makepyfile( """ @@ -1065,12 +1159,72 @@ def test_log_set_path(pytester: Pytester) -> None: """ ) pytester.runpytest() - with open(os.path.join(report_dir_base, "test_first")) as rfh: + with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh: + content = rfh.read() + assert "message from test 1" in content + + with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh: + content = rfh.read() + assert "message from test 2" in content + + +def test_log_set_path_with_log_file_mode(pytester: Pytester) -> None: + report_dir_base = str(pytester.path) + + pytester.makeini( + """ + [pytest] + log_file_level = DEBUG + log_cli=true + log_file_mode=a + """ + ) + pytester.makeconftest( + f""" + import os + import pytest + @pytest.hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_setup(item): + config = item.config + logging_plugin = config.pluginmanager.get_plugin("logging-plugin") + report_file = os.path.join({report_dir_base!r}, item._request.node.name) + logging_plugin.set_log_path(report_file) + return (yield) + """ + ) + pytester.makepyfile( + """ + import logging + logger = logging.getLogger("testcase-logger") + def test_first(): + logger.info("message from test 1") + assert True + + def test_second(): + logger.debug("message from test 2") + assert True + """ + ) + + test_first_log_file = os.path.join(report_dir_base, "test_first") + test_second_log_file = os.path.join(report_dir_base, "test_second") + with open(test_first_log_file, mode="w", encoding="utf-8") as wfh: + wfh.write("A custom header for test 1\n") + + with open(test_second_log_file, mode="w", encoding="utf-8") as wfh: + wfh.write("A custom header for test 2\n") + + result = pytester.runpytest() + assert result.ret == ExitCode.OK + + with open(test_first_log_file, encoding="utf-8") as rfh: content = rfh.read() + assert "A custom header for test 1" in content assert "message from test 1" in content - with open(os.path.join(report_dir_base, "test_second")) as rfh: + with open(test_second_log_file, encoding="utf-8") as rfh: content = rfh.read() + assert "A custom header for test 2" in content assert "message from test 2" in content @@ -1165,3 +1319,228 @@ def test_log_file_cli_subdirectories_are_successfully_created( result = pytester.runpytest("--log-file=foo/bar/logf.log") assert "logf.log" in os.listdir(expected) assert result.ret == ExitCode.OK + + +def test_disable_loggers(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import logging + import os + disabled_log = logging.getLogger('disabled') + test_log = logging.getLogger('test') + def test_logger_propagation(caplog): + with caplog.at_level(logging.DEBUG): + disabled_log.warning("no log; no stderr") + test_log.debug("Visible text!") + assert caplog.record_tuples == [('test', 10, 'Visible text!')] + """ + ) + result = pytester.runpytest("--log-disable=disabled", "-s") + assert result.ret == ExitCode.OK + assert not result.stderr.lines + + +def test_disable_loggers_does_not_propagate(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import logging + import os + + parent_logger = logging.getLogger("parent") + child_logger = parent_logger.getChild("child") + + def test_logger_propagation_to_parent(caplog): + with caplog.at_level(logging.DEBUG): + parent_logger.warning("some parent logger message") + child_logger.warning("some child logger message") + assert len(caplog.record_tuples) == 1 + assert caplog.record_tuples[0][0] == "parent" + assert caplog.record_tuples[0][2] == "some parent logger message" + """ + ) + + result = pytester.runpytest("--log-disable=parent.child", "-s") + assert result.ret == ExitCode.OK + assert not result.stderr.lines + + +def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import logging + disabled_log = logging.getLogger('disabled') + test_log = logging.getLogger('test') + + def test_log_cli_works(caplog): + test_log.info("Visible text!") + disabled_log.warning("This string will be suppressed.") + """ + ) + result = pytester.runpytest( + "--log-cli-level=DEBUG", + "--log-disable=disabled", + ) + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines( + "INFO test:test_log_disabling_works_with_log_cli.py:6 Visible text!" + ) + result.stdout.no_fnmatch_line( + "WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed." + ) + assert not result.stderr.lines + + +def test_without_date_format_log(pytester: Pytester) -> None: + """Check that date is not printed by default.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines( + ["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"] + ) + + +def test_date_format_log(pytester: Pytester) -> None: + """Check that log_date_format affects output.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"]) + + +def test_date_format_percentf_log(pytester: Pytester) -> None: + """Make sure that microseconds are printed in log.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S.%f + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"]) + + +def test_date_format_percentf_tz_log(pytester: Pytester) -> None: + """Make sure that timezone and microseconds are properly formatted together.""" + pytester.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.warning('text') + assert False + """ + ) + pytester.makeini( + """ + [pytest] + log_format=%(asctime)s; %(levelname)s; %(message)s + log_date_format=%Y-%m-%d %H:%M:%S.%f%z + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines( + [r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"] + ) + + +def test_log_file_cli_fallback_options(pytester: Pytester) -> None: + """Make sure that fallback values for log-file formats and level works.""" + pytester.makepyfile( + """ + import logging + logger = logging.getLogger() + + def test_foo(): + logger.info('info text going to logger') + logger.warning('warning text going to logger') + logger.error('error text going to logger') + + assert 0 + """ + ) + log_file = str(pytester.path.joinpath("pytest.log")) + result = pytester.runpytest( + "--log-level=ERROR", + "--log-format=%(asctime)s %(message)s", + "--log-date-format=%H:%M", + "--log-file=pytest.log", + ) + assert result.ret == 1 + + # The log file should only contain the error log messages + # not the warning or info ones and the format and date format + # should match the formats provided using --log-format and --log-date-format + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert re.match(r"[0-9]{2}:[0-9]{2} error text going to logger\s*", contents) + assert "info text going to logger" not in contents + assert "warning text going to logger" not in contents + assert "error text going to logger" in contents + + # Try with a different format and date format to make sure that the formats + # are being used + result = pytester.runpytest( + "--log-level=ERROR", + "--log-format=%(asctime)s : %(message)s", + "--log-date-format=%H:%M:%S", + "--log-file=pytest.log", + ) + assert result.ret == 1 + + # The log file should only contain the error log messages + # not the warning or info ones and the format and date format + # should match the formats provided using --log-format and --log-date-format + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert re.match( + r"[0-9]{2}:[0-9]{2}:[0-9]{2} : error text going to logger\s*", contents + ) + assert "info text going to logger" not in contents + assert "warning text going to logger" not in contents + assert "error text going to logger" in contents diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py index 35927ea5875..2bdb1545424 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/bdd_wallet.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs from pytest_bdd import given from pytest_bdd import scenario from pytest_bdd import then diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini index b42b07d145a..3bacdef62ab 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest.ini @@ -1,5 +1,6 @@ [pytest] addopts = --strict-markers +asyncio_mode = strict filterwarnings = error::pytest.PytestWarning ignore:.*.fspath is deprecated and will be replaced by .*.path.*:pytest.PytestDeprecationWarning diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py index 65c2f593663..383d7a0b5db 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_anyio_integration.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import anyio import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py index 5d2a3faccfc..b216c4beecd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_asyncio_integration.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import asyncio import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py index 740469d00fb..5494c44270a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_mock_integration.py @@ -1,2 +1,3 @@ +# mypy: allow-untyped-defs def test_mocker(mocker): mocker.MagicMock() diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py index 199f7850bc4..60f48ec609b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_trio_integration.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import trio import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py index 94748d036e5..0dbf5faeb8a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/pytest_twisted_integration.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest_twisted from twisted.internet.task import deferLater diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt index 90b253cc6d3..9e152f1191b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/requirements.txt @@ -1,15 +1,17 @@ -anyio[curio,trio]==3.4.0 -django==3.2.9 -pytest-asyncio==0.16.0 -pytest-bdd==5.0.0 -pytest-cov==3.0.0 -pytest-django==4.5.1 +anyio[curio,trio]==4.3.0 +django==5.0.4 +pytest-asyncio==0.23.6 +# Temporarily not installed until pytest-bdd is fixed: +# https://github.com/pytest-dev/pytest/pull/11785 +# pytest-bdd==7.0.1 +pytest-cov==5.0.0 +pytest-django==4.8.0 pytest-flakes==4.0.5 -pytest-html==3.1.1 -pytest-mock==3.6.1 -pytest-rerunfailures==10.2 -pytest-sugar==0.9.4 +pytest-html==4.1.1 +pytest-mock==3.14.0 +pytest-rerunfailures==14.0 +pytest-sugar==1.0.0 pytest-trio==0.7.0 -pytest-twisted==1.13.4 -twisted==21.7.0 -pytest-xvfb==2.0.0 +pytest-twisted==1.14.1 +twisted==24.3.0 +pytest-xvfb==3.0.0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py index 20b2fc4b5bb..48089afcc7e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/plugins_integration/simple_integration.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import pytest diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/approx.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/approx.py index 0d411d8a6da..968e8828512 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/approx.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/approx.py @@ -1,16 +1,19 @@ -import operator -import sys +# mypy: allow-untyped-defs from contextlib import contextmanager from decimal import Decimal from fractions import Fraction +from math import sqrt +import operator from operator import eq from operator import ne from typing import Optional -import pytest from _pytest.pytester import Pytester +from _pytest.python_api import _recursive_sequence_map +import pytest from pytest import approx + inf, nan = float("inf"), float("nan") @@ -36,9 +39,7 @@ def mocked_doctest_runner(monkeypatch): class MyDocTestRunner(doctest.DocTestRunner): def report_failure(self, out, test, example, got): raise AssertionError( - "'{}' evaluates to '{}', not '{}'".format( - example.source.strip(), got.strip(), example.want.strip() - ) + f"'{example.source.strip()}' evaluates to '{got.strip()}', not '{example.want.strip()}'" ) return MyDocTestRunner() @@ -93,13 +94,12 @@ SOME_INT = r"[0-9]+\s*" class TestApprox: - def test_error_messages(self, assert_approx_raises_regex): - np = pytest.importorskip("numpy") - + def test_error_messages_native_dtypes(self, assert_approx_raises_regex): assert_approx_raises_regex( 2.0, 1.0, [ + "", " comparison failed", f" Obtained: {SOME_FLOAT}", f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}", @@ -114,6 +114,7 @@ class TestApprox: "c": 3000000.0, }, [ + r"", r" comparison failed. Mismatched elements: 2 / 3:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", @@ -124,9 +125,28 @@ class TestApprox: ) assert_approx_raises_regex( + {"a": 1.0, "b": None, "c": None}, + { + "a": None, + "b": 1000.0, + "c": None, + }, + [ + r"", + r" comparison failed. Mismatched elements: 2 / 3:", + r" Max absolute difference: -inf", + r" Max relative difference: -inf", + r" Index \| Obtained\s+\| Expected\s+", + rf" a \| {SOME_FLOAT} \| None", + rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + + assert_approx_raises_regex( [1.0, 2.0, 3.0, 4.0], [1.0, 3.0, 3.0, 5.0], [ + r"", r" comparison failed. Mismatched elements: 2 / 4:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", @@ -136,6 +156,36 @@ class TestApprox: ], ) + assert_approx_raises_regex( + (1, 2.2, 4), + (1, 3.2, 4), + [ + r"", + r" comparison failed. Mismatched elements: 1 / 3:", + rf" Max absolute difference: {SOME_FLOAT}", + rf" Max relative difference: {SOME_FLOAT}", + r" Index \| Obtained\s+\| Expected ", + rf" 1 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + + # Specific test for comparison with 0.0 (relative diff will be 'inf') + assert_approx_raises_regex( + [0.0], + [1.0], + [ + r"", + r" comparison failed. Mismatched elements: 1 / 1:", + rf" Max absolute difference: {SOME_FLOAT}", + r" Max relative difference: inf", + r" Index \| Obtained\s+\| Expected ", + rf"\s*0\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + + def test_error_messages_numpy_dtypes(self, assert_approx_raises_regex): + np = pytest.importorskip("numpy") + a = np.linspace(0, 100, 20) b = np.linspace(0, 100, 20) a[10] += 0.5 @@ -143,6 +193,7 @@ class TestApprox: a, b, [ + r"", r" comparison failed. Mismatched elements: 1 / 20:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", @@ -165,6 +216,7 @@ class TestApprox: ] ), [ + r"", r" comparison failed. Mismatched elements: 3 / 8:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", @@ -177,21 +229,10 @@ class TestApprox: # Specific test for comparison with 0.0 (relative diff will be 'inf') assert_approx_raises_regex( - [0.0], - [1.0], - [ - r" comparison failed. Mismatched elements: 1 / 1:", - rf" Max absolute difference: {SOME_FLOAT}", - r" Max relative difference: inf", - r" Index \| Obtained\s+\| Expected ", - rf"\s*0\s*\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", - ], - ) - - assert_approx_raises_regex( np.array([0.0]), np.array([1.0]), [ + r"", r" comparison failed. Mismatched elements: 1 / 1:", rf" Max absolute difference: {SOME_FLOAT}", r" Max relative difference: inf", @@ -209,6 +250,7 @@ class TestApprox: message = "\n".join(str(e.value).split("\n")[1:]) assert message == "\n".join( [ + " ", " Impossible to compare arrays with different shapes.", " Shapes: (2, 1) and (2, 2)", ] @@ -219,6 +261,7 @@ class TestApprox: message = "\n".join(str(e.value).split("\n")[1:]) assert message == "\n".join( [ + " ", " Impossible to compare lists with different sizes.", " Lengths: 2 and 3", ] @@ -232,6 +275,7 @@ class TestApprox: 2.0, 1.0, [ + "", " comparison failed", f" Obtained: {SOME_FLOAT}", f" Expected: {SOME_FLOAT} ± {SOME_FLOAT}", @@ -245,15 +289,15 @@ class TestApprox: a, b, [ - r" comparison failed. Mismatched elements: 20 / 20:", - rf" Max absolute difference: {SOME_FLOAT}", - rf" Max relative difference: {SOME_FLOAT}", - r" Index \| Obtained\s+\| Expected", - rf" \(0,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", - rf" \(1,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", - rf" \(2,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}...", - "", - rf"\s*...Full output truncated \({SOME_INT} lines hidden\), use '-vv' to show", + r"^ $", + r"^ comparison failed. Mismatched elements: 20 / 20:$", + rf"^ Max absolute difference: {SOME_FLOAT}$", + rf"^ Max relative difference: {SOME_FLOAT}$", + r"^ Index \| Obtained\s+\| Expected\s+$", + rf"^ \(0,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}$", + rf"^ \(1,\)\s+\| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}e-{SOME_INT}\.\.\.$", + "^ $", + rf"^ ...Full output truncated \({SOME_INT} lines hidden\), use '-vv' to show$", ], verbosity_level=0, ) @@ -262,6 +306,7 @@ class TestApprox: a, b, [ + r" ", r" comparison failed. Mismatched elements: 20 / 20:", rf" Max absolute difference: {SOME_FLOAT}", rf" Max relative difference: {SOME_FLOAT}", @@ -615,6 +660,20 @@ class TestApprox: def test_dict_vs_other(self): assert 1 != approx({"a": 0}) + def test_dict_for_div_by_zero(self, assert_approx_raises_regex): + assert_approx_raises_regex( + {"foo": 42.0}, + {"foo": 0.0}, + [ + r"", + r" comparison failed. Mismatched elements: 1 / 1:", + rf" Max absolute difference: {SOME_FLOAT}", + r" Max relative difference: inf", + r" Index \| Obtained\s+\| Expected ", + rf" foo | {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + def test_numpy_array(self): np = pytest.importorskip("numpy") @@ -704,6 +763,23 @@ class TestApprox: assert a12 != approx(a21) assert a21 != approx(a12) + def test_numpy_array_implicit_conversion(self): + np = pytest.importorskip("numpy") + + class ImplicitArray: + """Type which is implicitly convertible to a numpy array.""" + + def __init__(self, vals): + self.vals = vals + + def __array__(self, dtype=None, copy=None): + return np.array(self.vals) + + vec1 = ImplicitArray([1.0, 2.0, 3.0]) + vec2 = ImplicitArray([1.0, 2.0, 4.0]) + # see issue #12114 for test case + assert vec1 != approx(vec2) + def test_numpy_array_protocol(self): """ array-like objects such as tensorflow's DeviceArray are handled like ndarray. @@ -772,7 +848,7 @@ class TestApprox: def test_expected_value_type_error(self, x, name): with pytest.raises( TypeError, - match=fr"pytest.approx\(\) does not support nested {name}:", + match=rf"pytest.approx\(\) does not support nested {name}:", ): approx(x) @@ -810,7 +886,6 @@ class TestApprox: assert 1.0 != approx([None]) assert None != approx([1.0]) # noqa: E711 - @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires ordered dicts") def test_nonnumeric_dict_repr(self): """Dicts with non-numerics and infinites have no tolerances""" x1 = {"foo": 1.0000005, "bar": None, "foobar": inf} @@ -860,13 +935,49 @@ class TestApprox: assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual - def test_generic_sized_iterable_object(self): - class MySizedIterable: - def __iter__(self): - return iter([1, 2, 3, 4]) + def test_generic_ordered_sequence(self): + class MySequence: + def __getitem__(self, i): + return [1, 2, 3, 4][i] def __len__(self): return 4 - expected = MySizedIterable() - assert [1, 2, 3, 4] == approx(expected) + expected = MySequence() + assert [1, 2, 3, 4] == approx(expected, abs=1e-4) + + expected_repr = "approx([1 ± 1.0e-06, 2 ± 2.0e-06, 3 ± 3.0e-06, 4 ± 4.0e-06])" + assert repr(approx(expected)) == expected_repr + + def test_allow_ordered_sequences_only(self) -> None: + """pytest.approx() should raise an error on unordered sequences (#9692).""" + with pytest.raises(TypeError, match="only supports ordered sequences"): + assert {1, 2, 3} == approx({1, 2, 3}) + + +class TestRecursiveSequenceMap: + def test_map_over_scalar(self): + assert _recursive_sequence_map(sqrt, 16) == 4 + + def test_map_over_empty_list(self): + assert _recursive_sequence_map(sqrt, []) == [] + + def test_map_over_list(self): + assert _recursive_sequence_map(sqrt, [4, 16, 25, 676]) == [2, 4, 5, 26] + + def test_map_over_tuple(self): + assert _recursive_sequence_map(sqrt, (4, 16, 25, 676)) == (2, 4, 5, 26) + + def test_map_over_nested_lists(self): + assert _recursive_sequence_map(sqrt, [4, [25, 64], [[49]]]) == [ + 2, + [5, 8], + [[7]], + ] + + def test_map_over_mixed_sequence(self): + assert _recursive_sequence_map(sqrt, [4, (25, 64), [(49)]]) == [ + 2, + (5, 8), + [(7)], + ] diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/collect.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/collect.py index ac3edd395ab..745550f0775 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/collect.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/collect.py @@ -1,3 +1,4 @@ +# mypy: allow-untyped-defs import os import sys import textwrap @@ -5,7 +6,6 @@ from typing import Any from typing import Dict import _pytest._code -import pytest from _pytest.config import ExitCode from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch @@ -13,6 +13,7 @@ from _pytest.nodes import Collector from _pytest.pytester import Pytester from _pytest.python import Class from _pytest.python import Function +import pytest class TestModule: @@ -53,14 +54,13 @@ class TestModule: monkeypatch.syspath_prepend(str(root1)) p.write_text( textwrap.dedent( - """\ + f"""\ import x456 def test(): - assert x456.__file__.startswith({!r}) - """.format( - str(root2) - ) - ) + assert x456.__file__.startswith({str(root2)!r}) + """ + ), + encoding="utf-8", ) with monkeypatch.context() as mp: mp.chdir(root2) @@ -775,13 +775,13 @@ class TestSorting: pytester.makepyfile( """\ class Test1: - def test_foo(): pass - def test_bar(): pass + def test_foo(self): pass + def test_bar(self): pass class Test2: - def test_foo(): pass + def test_foo(self): pass test_bar = Test1.test_bar class Test3(Test2): - def test_baz(): pass + def test_baz(self): pass """ ) result = pytester.runpytest("--collect-only") @@ -826,13 +826,14 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makemodule(): - outcome = yield - mod = outcome.get_result() + mod = yield mod.obj.hello = "world" + return mod """ - ) + ), + encoding="utf-8", ) b.joinpath("test_module.py").write_text( textwrap.dedent( @@ -840,7 +841,8 @@ class TestConftestCustomization: def test_hello(): assert hello == "world" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -852,16 +854,16 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(): - outcome = yield - if outcome.excinfo is None: - result = outcome.get_result() - if result: - for func in result: - func._some123 = "world" + result = yield + if result: + for func in result: + func._some123 = "world" + return result """ - ) + ), + encoding="utf-8", ) b.joinpath("test_module.py").write_text( textwrap.dedent( @@ -874,7 +876,8 @@ class TestConftestCustomization: def test_hello(obj): assert obj == "world" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -897,25 +900,29 @@ class TestConftestCustomization: def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None: """Ensure we can collect files with weird file extensions as Python modules (#2369)""" - # We'll implement a little finder and loader to import files containing + # Implement a little meta path finder to import files containing # Python source code whose file extension is ".narf". pytester.makeconftest( """ - import sys, os, imp + import sys + import os.path + from importlib.util import spec_from_loader + from importlib.machinery import SourceFileLoader from _pytest.python import Module - class Loader(object): - def load_module(self, name): - return imp.load_source(name, name + ".narf") - class Finder(object): - def find_module(self, name, path=None): - if os.path.exists(name + ".narf"): - return Loader() - sys.meta_path.append(Finder()) + class MetaPathFinder: + def find_spec(self, fullname, path, target=None): + if os.path.exists(fullname + ".narf"): + return spec_from_loader( + fullname, + SourceFileLoader(fullname, fullname + ".narf"), + ) + sys.meta_path.append(MetaPathFinder()) def pytest_collect_file(file_path, parent): if file_path.suffix == ".narf": - return Module.from_parent(path=file_path, parent=parent)""" + return Module.from_parent(path=file_path, parent=parent) + """ ) pytester.makefile( ".narf", @@ -970,7 +977,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None: def pytest_runtest_teardown(item): assert item.path.stem == "test_in_sub1" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("conftest.py").write_text( textwrap.dedent( @@ -983,10 +991,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None: def pytest_runtest_teardown(item): assert item.path.stem == "test_in_sub2" """ - ) + ), + encoding="utf-8", ) - sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8") + sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8") result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=2) @@ -1003,9 +1012,9 @@ class TestTracebackCutting: with pytest.raises(pytest.skip.Exception) as excinfo: pytest.skip("xxx") assert excinfo.traceback[-1].frame.code.name == "skip" - assert excinfo.traceback[-1].ishidden() + assert excinfo.traceback[-1].ishidden(excinfo) assert excinfo.traceback[-2].frame.code.name == "test_skip_simple" - assert not excinfo.traceback[-2].ishidden() + assert not excinfo.traceback[-2].ishidden(excinfo) def test_traceback_argsetup(self, pytester: Pytester) -> None: pytester.makeconftest( @@ -1200,7 +1209,7 @@ class TestReportInfo: classcol = pytester.collect_by_name(modcol, "TestClass") assert isinstance(classcol, Class) path, lineno, msg = classcol.reportinfo() - func = list(classcol.collect())[0] + func = next(iter(classcol.collect())) assert isinstance(func, Function) path, lineno, msg = func.reportinfo() @@ -1374,7 +1383,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None: def test_real(): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest(str(a), str(a)) result.stdout.fnmatch_lines(["*collected 1 item*"]) @@ -1394,7 +1404,8 @@ def test_keep_duplicates(pytester: Pytester) -> None: def test_real(): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("--keep-duplicates", str(a), str(a)) result.stdout.fnmatch_lines(["*collected 2 item*"]) @@ -1407,10 +1418,15 @@ def test_package_collection_infinite_recursion(pytester: Pytester) -> None: def test_package_collection_init_given_as_argument(pytester: Pytester) -> None: - """Regression test for #3749""" + """Regression test for #3749, #8976, #9263, #9313. + + Specifying an __init__.py file directly should collect only the __init__.py + Module, not the entire package. + """ p = pytester.copy_example("collect/package_init_given_as_arg") - result = pytester.runpytest(p / "pkg" / "__init__.py") - result.stdout.fnmatch_lines(["*1 passed*"]) + items, hookrecorder = pytester.inline_genitems(p / "pkg" / "__init__.py") + assert len(items) == 1 + assert items[0].name == "test_init" def test_package_with_modules(pytester: Pytester) -> None: @@ -1439,8 +1455,12 @@ def test_package_with_modules(pytester: Pytester) -> None: sub2_test = sub2.joinpath("test") sub2_test.mkdir(parents=True) - sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + sub1_test.joinpath("test_in_sub1.py").write_text( + "def test_1(): pass", encoding="utf-8" + ) + sub2_test.joinpath("test_in_sub2.py").write_text( + "def test_2(): pass", encoding="utf-8" + ) # Execute from . result = pytester.runpytest("-v", "-s") @@ -1484,10 +1504,117 @@ def test_package_ordering(pytester: Pytester) -> None: sub2_test = sub2.joinpath("test") sub2_test.mkdir(parents=True) - root.joinpath("Test_root.py").write_text("def test_1(): pass") - sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass") - sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass") + root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8") + sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8") + sub2_test.joinpath("test_sub2.py").write_text( + "def test_3(): pass", encoding="utf-8" + ) # Execute from . result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=3) + + +def test_collection_hierarchy(pytester: Pytester) -> None: + """A general test checking that a filesystem hierarchy is collected as + expected in various scenarios. + + top/ + ├── aaa + │ ├── pkg + │ │ ├── __init__.py + │ │ └── test_pkg.py + │ └── test_aaa.py + ├── test_a.py + ├── test_b + │ ├── __init__.py + │ └── test_b.py + ├── test_c.py + └── zzz + ├── dir + │ └── test_dir.py + ├── __init__.py + └── test_zzz.py + """ + pytester.makepyfile( + **{ + "top/aaa/test_aaa.py": "def test_it(): pass", + "top/aaa/pkg/__init__.py": "", + "top/aaa/pkg/test_pkg.py": "def test_it(): pass", + "top/test_a.py": "def test_it(): pass", + "top/test_b/__init__.py": "", + "top/test_b/test_b.py": "def test_it(): pass", + "top/test_c.py": "def test_it(): pass", + "top/zzz/__init__.py": "", + "top/zzz/test_zzz.py": "def test_it(): pass", + "top/zzz/dir/test_dir.py": "def test_it(): pass", + } + ) + + full = [ + "<Dir test_collection_hierarchy*>", + " <Dir top>", + " <Dir aaa>", + " <Package pkg>", + " <Module test_pkg.py>", + " <Function test_it>", + " <Module test_aaa.py>", + " <Function test_it>", + " <Module test_a.py>", + " <Function test_it>", + " <Package test_b>", + " <Module test_b.py>", + " <Function test_it>", + " <Module test_c.py>", + " <Function test_it>", + " <Package zzz>", + " <Dir dir>", + " <Module test_dir.py>", + " <Function test_it>", + " <Module test_zzz.py>", + " <Function test_it>", + ] + result = pytester.runpytest("--collect-only") + result.stdout.fnmatch_lines(full, consecutive=True) + result = pytester.runpytest("top", "--collect-only") + result.stdout.fnmatch_lines(full, consecutive=True) + result = pytester.runpytest("top", "top", "--collect-only") + result.stdout.fnmatch_lines(full, consecutive=True) + + result = pytester.runpytest( + "top/aaa", "top/aaa/pkg", "--collect-only", "--keep-duplicates" + ) + result.stdout.fnmatch_lines( + [ + "<Dir test_collection_hierarchy*>", + " <Dir top>", + " <Dir aaa>", + " <Package pkg>", + " <Module test_pkg.py>", + " <Function test_it>", + " <Module test_aaa.py>", + " <Function test_it>", + " <Package pkg>", + " <Module test_pkg.py>", + " <Function test_it>", + ], + consecutive=True, + ) + + result = pytester.runpytest( + "top/aaa/pkg", "top/aaa", "--collect-only", "--keep-duplicates" + ) + result.stdout.fnmatch_lines( + [ + "<Dir test_collection_hierarchy*>", + " <Dir top>", + " <Dir aaa>", + " <Package pkg>", + " <Module test_pkg.py>", + " <Function test_it>", + " <Function test_it>", + " <Module test_aaa.py>", + " <Function test_it>", + ], + consecutive=True, + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/fixtures.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/fixtures.py index f29ca1dfa59..741cf7dcf42 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/fixtures.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/fixtures.py @@ -1,17 +1,18 @@ +# mypy: allow-untyped-defs import os +from pathlib import Path import sys import textwrap -from pathlib import Path -import pytest -from _pytest import fixtures from _pytest.compat import getfuncargnames from _pytest.config import ExitCode -from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import deduplicate_names +from _pytest.fixtures import TopRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import get_public_names from _pytest.pytester import Pytester from _pytest.python import Function +import pytest def test_getfuncargnames_functions(): @@ -103,10 +104,6 @@ def test_getfuncargnames_staticmethod_partial(): @pytest.mark.pytester_example_path("fixtures/fill_fixtures") class TestFillFixtures: - def test_fillfuncargs_exposed(self): - # used by oejskit, kept for compatibility - assert pytest._fillfuncargs == fixtures._fillfuncargs - def test_funcarg_lookupfails(self, pytester: Pytester) -> None: pytester.copy_example() result = pytester.runpytest() # "--collect-only") @@ -291,7 +288,8 @@ class TestFillFixtures: def spam(): return 'spam' """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -300,7 +298,8 @@ class TestFillFixtures: def test_spam(spam): assert spam == "spam" """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -363,7 +362,8 @@ class TestFillFixtures: def spam(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -375,7 +375,8 @@ class TestFillFixtures: assert spam == params['spam'] params['spam'] += 1 """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) @@ -407,7 +408,8 @@ class TestFillFixtures: def spam(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -419,7 +421,8 @@ class TestFillFixtures: assert spam == params['spam'] params['spam'] += 1 """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) @@ -657,7 +660,7 @@ class TestRequestBasic: """ ) assert isinstance(item, Function) - req = fixtures.FixtureRequest(item, _ispytest=True) + req = TopRequest(item, _ispytest=True) assert req.function == item.obj assert req.keywords == item.keywords assert hasattr(req.module, "test_func") @@ -697,10 +700,9 @@ class TestRequestBasic: """ ) (item1,) = pytester.genitems([modcol]) + assert isinstance(item1, Function) assert item1.name == "test_method" - arg2fixturedefs = fixtures.FixtureRequest( - item1, _ispytest=True - )._arg2fixturedefs + arg2fixturedefs = TopRequest(item1, _ispytest=True)._arg2fixturedefs assert len(arg2fixturedefs) == 1 assert arg2fixturedefs["something"][0].argname == "something" @@ -710,7 +712,7 @@ class TestRequestBasic: ) def test_request_garbage(self, pytester: Pytester) -> None: try: - import xdist # noqa + import xdist # noqa: F401 except ImportError: pass else: @@ -930,8 +932,9 @@ class TestRequestBasic: self, pytester: Pytester ) -> None: """ - Ensure exceptions raised during teardown by a finalizer are suppressed - until all finalizers are called, re-raising the first exception (#2440) + Ensure exceptions raised during teardown by finalizers are suppressed + until all finalizers are called, then re-raised together in an + exception group (#2440) """ pytester.makepyfile( """ @@ -958,14 +961,23 @@ class TestRequestBasic: """ ) result = pytester.runpytest() + result.assert_outcomes(passed=2, errors=1) result.stdout.fnmatch_lines( - ["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"] + [ + ' | *ExceptionGroup: errors while tearing down fixture "subrequest" of <Function test_first> (2 sub-exceptions)', # noqa: E501 + " +-+---------------- 1 ----------------", + " | Exception: Error in something fixture", + " +---------------- 2 ----------------", + " | Exception: Error in excepts fixture", + " +------------------------------------", + ], ) def test_request_getmodulepath(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol("def test_somefunc(): pass") (item,) = pytester.genitems([modcol]) - req = fixtures.FixtureRequest(item, _ispytest=True) + assert isinstance(item, Function) + req = TopRequest(item, _ispytest=True) assert req.path == modcol.path def test_request_fixturenames(self, pytester: Pytester) -> None: @@ -1041,10 +1053,11 @@ class TestRequestBasic: def arg1(): pass """ - ) + ), + encoding="utf-8", ) p = b.joinpath("test_module.py") - p.write_text("def test_func(arg1): pass") + p.write_text("def test_func(arg1): pass", encoding="utf-8") result = pytester.runpytest(p, "--fixtures") assert result.ret == 0 result.stdout.fnmatch_lines( @@ -1122,7 +1135,8 @@ class TestRequestMarking: pass """ ) - req1 = fixtures.FixtureRequest(item1, _ispytest=True) + assert isinstance(item1, Function) + req1 = TopRequest(item1, _ispytest=True) assert "xfail" not in item1.keywords req1.applymarker(pytest.mark.xfail) assert "xfail" in item1.keywords @@ -1233,8 +1247,9 @@ class TestFixtureUsages: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*ScopeMismatch*involved factories*", + "*ScopeMismatch*Requesting fixture stack*", "test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)", + "Requested fixture:", "test_receives_funcargs_scope_mismatch.py:2: def arg1()", "*1 error*", ] @@ -1260,7 +1275,13 @@ class TestFixtureUsages: ) result = pytester.runpytest() result.stdout.fnmatch_lines( - ["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"] + [ + "*ScopeMismatch*Requesting fixture stack*", + "* def arg2(arg1)", + "Requested fixture:", + "* def arg1()", + "*1 error*", + ], ) def test_invalid_scope(self, pytester: Pytester) -> None: @@ -1283,7 +1304,7 @@ class TestFixtureUsages: @pytest.mark.parametrize("scope", ["function", "session"]) def test_parameters_without_eq_semantics(self, scope, pytester: Pytester) -> None: pytester.makepyfile( - """ + f""" class NoEq1: # fails on `a == b` statement def __eq__(self, _): raise RuntimeError @@ -1305,9 +1326,7 @@ class TestFixtureUsages: def test2(no_eq): pass - """.format( - scope=scope - ) + """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*4 passed*"]) @@ -1570,7 +1589,7 @@ class TestFixtureManagerParseFactories: """ def test_hello(item, fm): for name in ("fm", "hello", "item"): - faclist = fm.getfixturedefs(name, item.nodeid) + faclist = fm.getfixturedefs(name, item) assert len(faclist) == 1 fac = faclist[0] assert fac.func.__name__ == name @@ -1594,7 +1613,7 @@ class TestFixtureManagerParseFactories: def hello(self, request): return "class" def test_hello(self, item, fm): - faclist = fm.getfixturedefs("hello", item.nodeid) + faclist = fm.getfixturedefs("hello", item) print(faclist) assert len(faclist) == 3 @@ -1621,7 +1640,8 @@ class TestFixtureManagerParseFactories: def one(): return 1 """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1629,7 +1649,8 @@ class TestFixtureManagerParseFactories: def test_x(one): assert one == 1 """ - ) + ), + encoding="utf-8", ) sub = package.joinpath("sub") sub.mkdir() @@ -1642,7 +1663,8 @@ class TestFixtureManagerParseFactories: def one(): return 2 """ - ) + ), + encoding="utf-8", ) sub.joinpath("test_y.py").write_text( textwrap.dedent( @@ -1650,7 +1672,8 @@ class TestFixtureManagerParseFactories: def test_x(one): assert one == 2 """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1675,7 +1698,8 @@ class TestFixtureManagerParseFactories: def teardown_module(): values[:] = [] """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1684,7 +1708,8 @@ class TestFixtureManagerParseFactories: def test_x(): assert values == ["package"] """ - ) + ), + encoding="utf-8", ) package = pytester.mkdir("package2") package.joinpath("__init__.py").write_text( @@ -1696,7 +1721,8 @@ class TestFixtureManagerParseFactories: def teardown_module(): values[:] = [] """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1705,7 +1731,8 @@ class TestFixtureManagerParseFactories: def test_x(): assert values == ["package2"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1718,7 +1745,7 @@ class TestFixtureManagerParseFactories: ) pytester.syspathinsert(pytester.path.name) package = pytester.mkdir("package") - package.joinpath("__init__.py").write_text("") + package.joinpath("__init__.py").write_text("", encoding="utf-8") package.joinpath("conftest.py").write_text( textwrap.dedent( """\ @@ -1735,7 +1762,8 @@ class TestFixtureManagerParseFactories: yield values values.pop() """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1746,7 +1774,8 @@ class TestFixtureManagerParseFactories: def test_package(one): assert values == ["package-auto", "package"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1790,7 +1819,7 @@ class TestAutouseDiscovery: """ from _pytest.pytester import get_public_names def test_check_setup(item, fm): - autousenames = list(fm._getautousenames(item.nodeid)) + autousenames = list(fm._getautousenames(item)) assert len(get_public_names(autousenames)) == 2 assert "perfunction2" in autousenames assert "perfunction" in autousenames @@ -1896,8 +1925,12 @@ class TestAutouseDiscovery: """ ) conftest.rename(a.joinpath(conftest.name)) - a.joinpath("test_something.py").write_text("def test_func(): pass") - b.joinpath("test_otherthing.py").write_text("def test_func(): pass") + a.joinpath("test_something.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) + b.joinpath("test_otherthing.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) result = pytester.runpytest() result.stdout.fnmatch_lines( """ @@ -1943,7 +1976,8 @@ class TestAutouseManagement: import sys sys._myapp = "hello" """ - ) + ), + encoding="utf-8", ) sub = pkgdir.joinpath("tests") sub.mkdir() @@ -1956,7 +1990,8 @@ class TestAutouseManagement: def test_app(): assert sys._myapp == "hello" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) @@ -2081,9 +2116,7 @@ class TestAutouseManagement: reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path) reprec.assertoutcome(passed=8) config = reprec.getcalls("pytest_unconfigure")[0].config - values = config.pluginmanager._getconftestmodules( - p, importmode="prepend", rootpath=pytester.path - )[0].values + values = config.pluginmanager._getconftestmodules(p)[0].values assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 def test_scope_ordering(self, pytester: Pytester) -> None: @@ -2180,7 +2213,7 @@ class TestAutouseManagement: pass def test_check(): assert values == ["new1", "new2", "fin2", "fin1"] - """ + """ # noqa: UP031 (python syntax issues) % locals() ) reprec = pytester.inline_run("-s") @@ -2442,6 +2475,33 @@ class TestFixtureMarker: ["*ScopeMismatch*You tried*function*session*request*"] ) + def test_scope_mismatch_already_computed_dynamic(self, pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import pytest + + @pytest.fixture(scope="function") + def fixfunc(): pass + + @pytest.fixture(scope="module") + def fixmod(fixfunc): pass + + def test_it(request, fixfunc): + request.getfixturevalue("fixmod") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + result.stdout.fnmatch_lines( + [ + "*ScopeMismatch*Requesting fixture stack*", + "test_it.py:6: def fixmod(fixfunc)", + "Requested fixture:", + "test_it.py:3: def fixfunc()", + ] + ) + def test_dynamic_scope(self, pytester: Pytester) -> None: pytester.makeconftest( """ @@ -2688,12 +2748,12 @@ class TestFixtureMarker: """ test_dynamic_parametrized_ordering.py::test[flavor1-vxlan] PASSED test_dynamic_parametrized_ordering.py::test2[flavor1-vxlan] PASSED - test_dynamic_parametrized_ordering.py::test[flavor2-vxlan] PASSED - test_dynamic_parametrized_ordering.py::test2[flavor2-vxlan] PASSED - test_dynamic_parametrized_ordering.py::test[flavor2-vlan] PASSED - test_dynamic_parametrized_ordering.py::test2[flavor2-vlan] PASSED test_dynamic_parametrized_ordering.py::test[flavor1-vlan] PASSED test_dynamic_parametrized_ordering.py::test2[flavor1-vlan] PASSED + test_dynamic_parametrized_ordering.py::test[flavor2-vlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor2-vlan] PASSED + test_dynamic_parametrized_ordering.py::test[flavor2-vxlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor2-vxlan] PASSED """ ) @@ -2886,7 +2946,7 @@ class TestFixtureMarker: def browser(request): def finalize(): - sys.stdout.write_text('Finalized') + sys.stdout.write_text('Finalized', encoding='utf-8') request.addfinalizer(finalize) return {} """ @@ -2904,7 +2964,8 @@ class TestFixtureMarker: def test_browser(browser): assert browser['visited'] is True """ - ) + ), + encoding="utf-8", ) reprec = pytester.runpytest("-s") for test in ["test_browser"]: @@ -3042,7 +3103,7 @@ class TestFixtureMarker: pass def test_other(): pass - """ + """ # noqa: UP031 (python syntax issues) % {"scope": scope} ) reprec = pytester.inline_run("-lvs") @@ -3242,7 +3303,7 @@ class TestRequestScopeAccess: assert request.config def test_func(): pass - """ + """ # noqa: UP031 (python syntax issues) % (scope, ok.split(), error.split()) ) reprec = pytester.inline_run("-l") @@ -3263,7 +3324,7 @@ class TestRequestScopeAccess: assert request.config def test_func(arg): pass - """ + """ # noqa: UP031 (python syntax issues) % (scope, ok.split(), error.split()) ) reprec = pytester.inline_run() @@ -3342,6 +3403,10 @@ class TestShowFixtures: config = pytester.parseconfigure("--funcargs") assert config.option.showfixtures + def test_show_help(self, pytester: Pytester) -> None: + result = pytester.runpytest("--fixtures", "--help") + assert not result.ret + def test_show_fixtures(self, pytester: Pytester) -> None: result = pytester.runpytest("--fixtures") result.stdout.fnmatch_lines( @@ -3855,7 +3920,8 @@ class TestParameterizedSubRequest: def fix_with_param(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = tests_dir.joinpath("test_foos.py") @@ -3867,7 +3933,8 @@ class TestParameterizedSubRequest: def test_foo(request): request.getfixturevalue('fix_with_param') """ - ) + ), + encoding="utf-8", ) os.chdir(tests_dir) @@ -3983,7 +4050,8 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() def test_func_closure_with_native_fixtures( @@ -4031,7 +4099,8 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) # order of fixtures based on their scope and position in the parameter list assert ( request.fixturenames @@ -4058,7 +4127,8 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() def test_func_closure_scopes_reordered(self, pytester: Pytester) -> None: @@ -4091,7 +4161,8 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 m1 c1 f2 f1".split() def test_func_closure_same_scope_closer_root_first( @@ -4133,7 +4204,8 @@ class TestScopeOrdering: } ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split() def test_func_closure_all_scopes_complex(self, pytester: Pytester) -> None: @@ -4177,7 +4249,8 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() - request = FixtureRequest(items[0], _ispytest=True) + assert isinstance(items[0], Function) + request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() def test_multiple_packages(self, pytester: Pytester) -> None: @@ -4196,7 +4269,7 @@ class TestScopeOrdering: └── test_2.py """ root = pytester.mkdir("root") - root.joinpath("__init__.py").write_text("values = []") + root.joinpath("__init__.py").write_text("values = []", encoding="utf-8") sub1 = root.joinpath("sub1") sub1.mkdir() sub1.joinpath("__init__.py").touch() @@ -4211,7 +4284,8 @@ class TestScopeOrdering: yield values assert values.pop() == "pre-sub1" """ - ) + ), + encoding="utf-8", ) sub1.joinpath("test_1.py").write_text( textwrap.dedent( @@ -4220,7 +4294,8 @@ class TestScopeOrdering: def test_1(fix): assert values == ["pre-sub1"] """ - ) + ), + encoding="utf-8", ) sub2 = root.joinpath("sub2") sub2.mkdir() @@ -4236,7 +4311,8 @@ class TestScopeOrdering: yield values assert values.pop() == "pre-sub2" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("test_2.py").write_text( textwrap.dedent( @@ -4245,7 +4321,8 @@ class TestScopeOrdering: def test_2(fix): assert values == ["pre-sub2"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -4294,6 +4371,27 @@ def test_call_fixture_function_error(): assert fix() == 1 +def test_fixture_double_decorator(pytester: Pytester) -> None: + """Check if an error is raised when using @pytest.fixture twice.""" + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + @pytest.fixture + def fixt(): + pass + """ + ) + result = pytester.runpytest() + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines( + [ + "E * ValueError: @pytest.fixture is being applied more than once to the same function 'fixt'" + ] + ) + + def test_fixture_param_shadowing(pytester: Pytester) -> None: """Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)""" pytester.makepyfile( @@ -4343,7 +4441,7 @@ def test_fixture_named_request(pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "*'request' is a reserved word for fixtures, use another name:", - " *test_fixture_named_request.py:5", + " *test_fixture_named_request.py:6", ] ) @@ -4472,3 +4570,236 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: result.assert_outcomes(errors=1) result.stdout.fnmatch_lines([expected]) assert result.ret == ExitCode.TESTS_FAILED + + +def test_deduplicate_names() -> None: + items = deduplicate_names("abacd") + assert items == ("a", "b", "c", "d") + items = deduplicate_names((*items, "g", "f", "g", "e", "b")) + assert items == ("a", "b", "c", "d", "g", "f", "e") + + +def test_staticmethod_classmethod_fixture_instance(pytester: Pytester) -> None: + """Ensure that static and class methods get and have access to a fresh + instance. + + This also ensures `setup_method` works well with static and class methods. + + Regression test for #12065. + """ + pytester.makepyfile( + """ + import pytest + + class Test: + ran_setup_method = False + ran_fixture = False + + def setup_method(self): + assert not self.ran_setup_method + self.ran_setup_method = True + + @pytest.fixture(autouse=True) + def fixture(self): + assert not self.ran_fixture + self.ran_fixture = True + + def test_method(self): + assert self.ran_setup_method + assert self.ran_fixture + + @staticmethod + def test_1(request): + assert request.instance.ran_setup_method + assert request.instance.ran_fixture + + @classmethod + def test_2(cls, request): + assert request.instance.ran_setup_method + assert request.instance.ran_fixture + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=3) + + +def test_scoped_fixture_caching(pytester: Pytester) -> None: + """Make sure setup and finalization is only run once when using scoped fixture + multiple times.""" + pytester.makepyfile( + """ + from __future__ import annotations + + from typing import Generator + + import pytest + executed: list[str] = [] + @pytest.fixture(scope="class") + def fixture_1() -> Generator[None, None, None]: + executed.append("fix setup") + yield + executed.append("fix teardown") + + + class TestFixtureCaching: + def test_1(self, fixture_1: None) -> None: + assert executed == ["fix setup"] + + def test_2(self, fixture_1: None) -> None: + assert executed == ["fix setup"] + + + def test_expected_setup_and_teardown() -> None: + assert executed == ["fix setup", "fix teardown"] + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + +def test_scoped_fixture_caching_exception(pytester: Pytester) -> None: + """Make sure setup & finalization is only run once for scoped fixture, with a cached exception.""" + pytester.makepyfile( + """ + from __future__ import annotations + + import pytest + executed_crash: list[str] = [] + + + @pytest.fixture(scope="class") + def fixture_crash(request: pytest.FixtureRequest) -> None: + executed_crash.append("fix_crash setup") + + def my_finalizer() -> None: + executed_crash.append("fix_crash teardown") + + request.addfinalizer(my_finalizer) + + raise Exception("foo") + + + class TestFixtureCachingException: + @pytest.mark.xfail + def test_crash_1(self, fixture_crash: None) -> None: + ... + + @pytest.mark.xfail + def test_crash_2(self, fixture_crash: None) -> None: + ... + + + def test_crash_expected_setup_and_teardown() -> None: + assert executed_crash == ["fix_crash setup", "fix_crash teardown"] + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + +def test_scoped_fixture_teardown_order(pytester: Pytester) -> None: + """ + Make sure teardowns happen in reverse order of setup with scoped fixtures, when + a later test only depends on a subset of scoped fixtures. + + Regression test for https://github.com/pytest-dev/pytest/issues/1489 + """ + pytester.makepyfile( + """ + from typing import Generator + + import pytest + + + last_executed = "" + + + @pytest.fixture(scope="module") + def fixture_1() -> Generator[None, None, None]: + global last_executed + assert last_executed == "" + last_executed = "fixture_1_setup" + yield + assert last_executed == "fixture_2_teardown" + last_executed = "fixture_1_teardown" + + + @pytest.fixture(scope="module") + def fixture_2() -> Generator[None, None, None]: + global last_executed + assert last_executed == "fixture_1_setup" + last_executed = "fixture_2_setup" + yield + assert last_executed == "run_test" + last_executed = "fixture_2_teardown" + + + def test_fixture_teardown_order(fixture_1: None, fixture_2: None) -> None: + global last_executed + assert last_executed == "fixture_2_setup" + last_executed = "run_test" + + + def test_2(fixture_1: None) -> None: + # This would previously queue an additional teardown of fixture_1, + # despite fixture_1's value being cached, which caused fixture_1 to be + # torn down before fixture_2 - violating the rule that teardowns should + # happen in reverse order of setup. + pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + +def test_subfixture_teardown_order(pytester: Pytester) -> None: + """ + Make sure fixtures don't re-register their finalization in parent fixtures multiple + times, causing ordering failure in their teardowns. + + Regression test for #12135 + """ + pytester.makepyfile( + """ + import pytest + + execution_order = [] + + @pytest.fixture(scope="class") + def fixture_1(): + ... + + @pytest.fixture(scope="class") + def fixture_2(fixture_1): + execution_order.append("setup 2") + yield + execution_order.append("teardown 2") + + @pytest.fixture(scope="class") + def fixture_3(fixture_1): + execution_order.append("setup 3") + yield + execution_order.append("teardown 3") + + class TestFoo: + def test_initialize_fixtures(self, fixture_2, fixture_3): + ... + + # This would previously reschedule fixture_2's finalizer in the parent fixture, + # causing it to be torn down before fixture 3. + def test_reschedule_fixture_2(self, fixture_2): + ... + + # Force finalization directly on fixture_1 + # Otherwise the cleanup would sequence 3&2 before 1 as normal. + @pytest.mark.parametrize("fixture_1", [None], indirect=["fixture_1"]) + def test_finalize_fixture_1(self, fixture_1): + ... + + def test_result(): + assert execution_order == ["setup 2", "setup 3", "teardown 3", "teardown 2"] + """ + ) + result = pytester.runpytest() + assert result.ret == 0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/integration.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/integration.py index d138b726638..c20aaeed839 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/integration.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/integration.py @@ -1,82 +1,9 @@ -from typing import Any - -import pytest -from _pytest import runner +# mypy: allow-untyped-defs from _pytest._code import getfslineno from _pytest.fixtures import getfixturemarker from _pytest.pytester import Pytester from _pytest.python import Function - - -class TestOEJSKITSpecials: - def test_funcarg_non_pycollectobj( - self, pytester: Pytester, recwarn - ) -> None: # rough jstests usage - pytester.makeconftest( - """ - import pytest - def pytest_pycollect_makeitem(collector, name, obj): - if name == "MyClass": - return MyCollector.from_parent(collector, name=name) - class MyCollector(pytest.Collector): - def reportinfo(self): - return self.path, 3, "xyz" - """ - ) - modcol = pytester.getmodulecol( - """ - import pytest - @pytest.fixture - def arg1(request): - return 42 - class MyClass(object): - pass - """ - ) - # this hook finds funcarg factories - rep = runner.collect_one_node(collector=modcol) - # TODO: Don't treat as Any. - clscol: Any = rep.result[0] - clscol.obj = lambda arg1: None - clscol.funcargs = {} - pytest._fillfuncargs(clscol) - assert clscol.funcargs["arg1"] == 42 - - def test_autouse_fixture( - self, pytester: Pytester, recwarn - ) -> None: # rough jstests usage - pytester.makeconftest( - """ - import pytest - def pytest_pycollect_makeitem(collector, name, obj): - if name == "MyClass": - return MyCollector.from_parent(collector, name=name) - class MyCollector(pytest.Collector): - def reportinfo(self): - return self.path, 3, "xyz" - """ - ) - modcol = pytester.getmodulecol( - """ - import pytest - @pytest.fixture(autouse=True) - def hello(): - pass - @pytest.fixture - def arg1(request): - return 42 - class MyClass(object): - pass - """ - ) - # this hook finds funcarg factories - rep = runner.collect_one_node(modcol) - # TODO: Don't treat as Any. - clscol: Any = rep.result[0] - clscol.obj = lambda: None - clscol.funcargs = {} - pytest._fillfuncargs(clscol) - assert not clscol.funcargs +import pytest def test_wrapped_getfslineno() -> None: @@ -116,9 +43,10 @@ class TestMockDecoration: assert values == ("x",) def test_getfuncargnames_patching(self): - from _pytest.compat import getfuncargnames from unittest.mock import patch + from _pytest.compat import getfuncargnames + class T: def original(self, x, y, z): pass @@ -235,7 +163,7 @@ class TestMockDecoration: @mock.patch("os.path.abspath") @mock.patch("os.path.normpath") @mock.patch("os.path.basename", new=mock_basename) - def test_someting(normpath, abspath, tmp_path): + def test_something(normpath, abspath, tmp_path): abspath.return_value = "this" os.path.normpath(os.path.abspath("hello")) normpath.assert_any_call("this") @@ -248,7 +176,7 @@ class TestMockDecoration: funcnames = [ call.report.location[2] for call in calls if call.report.when == "call" ] - assert funcnames == ["T.test_hello", "test_someting"] + assert funcnames == ["T.test_hello", "test_something"] def test_mock_sorting(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") @@ -482,22 +410,37 @@ def test_function_instance(pytester: Pytester) -> None: items = pytester.getitems( """ def test_func(): pass + class TestIt: def test_method(self): pass + @classmethod def test_class(cls): pass + @staticmethod def test_static(): pass """ ) - assert len(items) == 3 + assert len(items) == 4 + assert isinstance(items[0], Function) assert items[0].name == "test_func" assert items[0].instance is None + assert isinstance(items[1], Function) assert items[1].name == "test_method" assert items[1].instance is not None assert items[1].instance.__class__.__name__ == "TestIt" + + # Even class and static methods get an instance! + # This is the instance used for bound fixture methods, which + # class/staticmethod tests are perfectly able to request. assert isinstance(items[2], Function) - assert items[2].name == "test_static" - assert items[2].instance is None + assert items[2].name == "test_class" + assert items[2].instance is not None + + assert isinstance(items[3], Function) + assert items[3].name == "test_static" + assert items[3].instance is not None + + assert items[1].instance is not items[2].instance is not items[3].instance diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/metafunc.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/metafunc.py index fc0082eb6b9..3d0058fa0a7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/metafunc.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/metafunc.py @@ -1,3 +1,5 @@ +# mypy: allow-untyped-defs +import dataclasses import itertools import re import sys @@ -12,21 +14,19 @@ from typing import Sequence from typing import Tuple from typing import Union -import attr import hypothesis from hypothesis import strategies -import pytest from _pytest import fixtures from _pytest import python -from _pytest.compat import _format_args from _pytest.compat import getfuncargnames from _pytest.compat import NOTSET from _pytest.outcomes import fail from _pytest.pytester import Pytester -from _pytest.python import _idval -from _pytest.python import idmaker +from _pytest.python import Function +from _pytest.python import IdMaker from _pytest.scope import Scope +import pytest class TestMetafunc: @@ -35,19 +35,29 @@ class TestMetafunc: # on the funcarg level, so we don't need a full blown # initialization. class FuncFixtureInfoMock: - name2fixturedefs = None + name2fixturedefs: Dict[str, List[fixtures.FixtureDef[object]]] = {} def __init__(self, names): self.names_closure = names - @attr.s + @dataclasses.dataclass + class FixtureManagerMock: + config: Any + + @dataclasses.dataclass + class SessionMock: + _fixturemanager: FixtureManagerMock + + @dataclasses.dataclass class DefinitionMock(python.FunctionDefinition): - obj = attr.ib() - _nodeid = attr.ib() + _nodeid: str + obj: object names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) - definition: Any = DefinitionMock._create(func, "mock::nodeid") + definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid") + definition._fixtureinfo = fixtureinfo + definition.session = SessionMock(FixtureManagerMock({})) return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) def test_no_funcargs(self) -> None: @@ -99,19 +109,19 @@ class TestMetafunc: metafunc = self.Metafunc(func) # When the input is an iterator, only len(args) are taken, # so the bad Exc isn't reached. - metafunc.parametrize("x", [1, 2], ids=gen()) # type: ignore[arg-type] - assert [(x.funcargs, x.id) for x in metafunc._calls] == [ + metafunc.parametrize("x", [1, 2], ids=gen()) + assert [(x.params, x.id) for x in metafunc._calls] == [ ({"x": 1}, "0"), ({"x": 2}, "2"), ] with pytest.raises( fail.Exception, match=( - r"In func: ids must be list of string/float/int/bool, found:" - r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2" + r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. " + r"Supported types are: .*" ), ): - metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type] + metafunc.parametrize("x", [1, 2, 3], ids=gen()) def test_parametrize_bad_scope(self) -> None: def func(x): @@ -141,9 +151,9 @@ class TestMetafunc: """Unit test for _find_parametrized_scope (#3941).""" from _pytest.python import _find_parametrized_scope - @attr.s + @dataclasses.dataclass class DummyFixtureDef: - _scope = attr.ib() + _scope: Scope fixtures_defs = cast( Dict[str, Sequence[fixtures.FixtureDef[object]]], @@ -153,6 +163,7 @@ class TestMetafunc: module_fix=[DummyFixtureDef(Scope.Module)], class_fix=[DummyFixtureDef(Scope.Class)], func_fix=[DummyFixtureDef(Scope.Function)], + mixed_fix=[DummyFixtureDef(Scope.Module), DummyFixtureDef(Scope.Class)], ), ) @@ -189,6 +200,7 @@ class TestMetafunc: ) == Scope.Module ) + assert find_scope(["mixed_fix"], indirect=True) == Scope.Class def test_parametrize_and_id(self) -> None: def func(x, y): @@ -286,7 +298,7 @@ class TestMetafunc: deadline=400.0 ) # very close to std deadline and CI boxes are not reliable in CPU power def test_idval_hypothesis(self, value) -> None: - escaped = _idval(value, "a", 6, None, nodeid=None, config=None) + escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6) assert isinstance(escaped, str) escaped.encode("ascii") @@ -308,7 +320,10 @@ class TestMetafunc: ), ] for val, expected in values: - assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected + assert ( + IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6) + == expected + ) def test_unicode_idval_with_config(self) -> None: """Unit test for expected behavior to obtain ids with @@ -336,7 +351,7 @@ class TestMetafunc: ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), ] for val, config, expected in values: - actual = _idval(val, "a", 6, None, nodeid=None, config=config) + actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6) assert actual == expected def test_bytes_idval(self) -> None: @@ -349,7 +364,10 @@ class TestMetafunc: ("αρά".encode(), r"\xce\xb1\xcf\x81\xce\xac"), ] for val, expected in values: - assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected + assert ( + IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6) + == expected + ) def test_class_or_function_idval(self) -> None: """Unit test for the expected behavior to obtain ids for parametrized @@ -363,7 +381,10 @@ class TestMetafunc: values = [(TestClass, "TestClass"), (test_function, "test_function")] for val, expected in values: - assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected + assert ( + IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6) + == expected + ) def test_notset_idval(self) -> None: """Test that a NOTSET value (used by an empty parameterset) generates @@ -371,29 +392,47 @@ class TestMetafunc: Regression test for #7686. """ - assert _idval(NOTSET, "a", 0, None, nodeid=None, config=None) == "a0" + assert ( + IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0" + ) def test_idmaker_autoname(self) -> None: """#250""" - result = idmaker( - ("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)] - ) + result = IdMaker( + ("a", "b"), + [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)], + None, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["string-1.0", "st-ring-2.0"] - result = idmaker( - ("a", "b"), [pytest.param(object(), 1.0), pytest.param(object(), object())] - ) + result = IdMaker( + ("a", "b"), + [pytest.param(object(), 1.0), pytest.param(object(), object())], + None, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["a0-1.0", "a1-b1"] # unicode mixing, issue250 - result = idmaker(("a", "b"), [pytest.param({}, b"\xc3\xb4")]) + result = IdMaker( + ("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None + ).make_unique_parameterset_ids() assert result == ["a0-\\xc3\\xb4"] def test_idmaker_with_bytes_regex(self) -> None: - result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)]) + result = IdMaker( + ("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None + ).make_unique_parameterset_ids() assert result == ["foo"] def test_idmaker_native_strings(self) -> None: - result = idmaker( + result = IdMaker( ("a", "b"), [ pytest.param(1.0, -1.1), @@ -410,7 +449,12 @@ class TestMetafunc: pytest.param(b"\xc3\xb4", "other"), pytest.param(1.0j, -2.0j), ], - ) + None, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == [ "1.0--1.1", "2--202", @@ -428,7 +472,7 @@ class TestMetafunc: ] def test_idmaker_non_printable_characters(self) -> None: - result = idmaker( + result = IdMaker( ("s", "n"), [ pytest.param("\x00", 1), @@ -438,23 +482,35 @@ class TestMetafunc: pytest.param("\t", 5), pytest.param(b"\t", 6), ], - ) + None, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"] def test_idmaker_manual_ids_must_be_printable(self) -> None: - result = idmaker( + result = IdMaker( ("s",), [ pytest.param("x00", id="hello \x00"), pytest.param("x05", id="hello \x05"), ], - ) + None, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["hello \\x00", "hello \\x05"] def test_idmaker_enum(self) -> None: enum = pytest.importorskip("enum") e = enum.Enum("Foo", "one, two") - result = idmaker(("a", "b"), [pytest.param(e.one, e.two)]) + result = IdMaker( + ("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None + ).make_unique_parameterset_ids() assert result == ["Foo.one-Foo.two"] def test_idmaker_idfn(self) -> None: @@ -465,15 +521,19 @@ class TestMetafunc: return repr(val) return None - result = idmaker( + result = IdMaker( ("a", "b"), [ pytest.param(10.0, IndexError()), pytest.param(20, KeyError()), pytest.param("three", [1, 2, 3]), ], - idfn=ids, - ) + ids, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"] def test_idmaker_idfn_unique_names(self) -> None: @@ -482,15 +542,19 @@ class TestMetafunc: def ids(val: object) -> str: return "a" - result = idmaker( + result = IdMaker( ("a", "b"), [ pytest.param(10.0, IndexError()), pytest.param(20, KeyError()), pytest.param("three", [1, 2, 3]), ], - idfn=ids, - ) + ids, + None, + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["a-a0", "a-a1", "a-a2"] def test_idmaker_with_idfn_and_config(self) -> None: @@ -520,12 +584,15 @@ class TestMetafunc: (MockConfig({option: False}), "a\\xe7\\xe3o"), ] for config, expected in values: - result = idmaker( + result = IdMaker( ("a",), [pytest.param("string")], - idfn=lambda _: "ação", - config=config, - ) + lambda _: "ação", + None, + config, + None, + None, + ).make_unique_parameterset_ids() assert result == [expected] def test_idmaker_with_ids_and_config(self) -> None: @@ -555,14 +622,18 @@ class TestMetafunc: (MockConfig({option: False}), "a\\xe7\\xe3o"), ] for config, expected in values: - result = idmaker( - ("a",), - [pytest.param("string")], - ids=["ação"], - config=config, - ) + result = IdMaker( + ("a",), [pytest.param("string")], None, ["ação"], config, None, None + ).make_unique_parameterset_ids() assert result == [expected] + def test_idmaker_duplicated_empty_str(self) -> None: + """Regression test for empty strings parametrized more than once (#11563).""" + result = IdMaker( + ("a",), [pytest.param(""), pytest.param("")], None, None, None, None, None + ).make_unique_parameterset_ids() + assert result == ["0", "1"] + def test_parametrize_ids_exception(self, pytester: Pytester) -> None: """ :param pytester: the instance of Pytester class, a temporary @@ -617,23 +688,39 @@ class TestMetafunc: ) def test_idmaker_with_ids(self) -> None: - result = idmaker( - ("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None] - ) + result = IdMaker( + ("a", "b"), + [pytest.param(1, 2), pytest.param(3, 4)], + None, + ["a", None], + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["a", "3-4"] def test_idmaker_with_paramset_id(self) -> None: - result = idmaker( + result = IdMaker( ("a", "b"), [pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")], - ids=["a", None], - ) + None, + ["a", None], + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["me", "you"] def test_idmaker_with_ids_unique_names(self) -> None: - result = idmaker( - ("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"] - ) + result = IdMaker( + ("a"), + list(map(pytest.param, [1, 2, 3, 4, 5])), + None, + ["a", "a", "b", "c", "b"], + None, + None, + None, + ).make_unique_parameterset_ids() assert result == ["a0", "a1", "b0", "c", "b1"] def test_parametrize_indirect(self) -> None: @@ -646,8 +733,6 @@ class TestMetafunc: metafunc.parametrize("x", [1], indirect=True) metafunc.parametrize("y", [2, 3], indirect=True) assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == {} - assert metafunc._calls[1].funcargs == {} assert metafunc._calls[0].params == dict(x=1, y=2) assert metafunc._calls[1].params == dict(x=1, y=3) @@ -659,8 +744,10 @@ class TestMetafunc: metafunc = self.Metafunc(func) metafunc.parametrize("x, y", [("a", "b")], indirect=["x"]) - assert metafunc._calls[0].funcargs == dict(y="b") - assert metafunc._calls[0].params == dict(x="a") + assert metafunc._calls[0].params == dict(x="a", y="b") + # Since `y` is a direct parameter, its pseudo-fixture would + # be registered. + assert list(metafunc._arg2fixturedefs.keys()) == ["y"] def test_parametrize_indirect_list_all(self) -> None: """#714""" @@ -670,8 +757,8 @@ class TestMetafunc: metafunc = self.Metafunc(func) metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "y"]) - assert metafunc._calls[0].funcargs == {} assert metafunc._calls[0].params == dict(x="a", y="b") + assert list(metafunc._arg2fixturedefs.keys()) == [] def test_parametrize_indirect_list_empty(self) -> None: """#714""" @@ -681,8 +768,8 @@ class TestMetafunc: metafunc = self.Metafunc(func) metafunc.parametrize("x, y", [("a", "b")], indirect=[]) - assert metafunc._calls[0].funcargs == dict(x="a", y="b") - assert metafunc._calls[0].params == {} + assert metafunc._calls[0].params == dict(x="a", y="b") + assert list(metafunc._arg2fixturedefs.keys()) == ["x", "y"] def test_parametrize_indirect_wrong_type(self) -> None: def func(x, y): @@ -876,9 +963,9 @@ class TestMetafunc: metafunc = self.Metafunc(lambda x: None) metafunc.parametrize("x", [1, 2]) assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == dict(x=1) + assert metafunc._calls[0].params == dict(x=1) assert metafunc._calls[0].id == "1" - assert metafunc._calls[1].funcargs == dict(x=2) + assert metafunc._calls[1].params == dict(x=2) assert metafunc._calls[1].id == "2" def test_parametrize_onearg_indirect(self) -> None: @@ -893,11 +980,45 @@ class TestMetafunc: metafunc = self.Metafunc(lambda x, y: None) metafunc.parametrize(("x", "y"), [(1, 2), (3, 4)]) assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == dict(x=1, y=2) + assert metafunc._calls[0].params == dict(x=1, y=2) assert metafunc._calls[0].id == "1-2" - assert metafunc._calls[1].funcargs == dict(x=3, y=4) + assert metafunc._calls[1].params == dict(x=3, y=4) assert metafunc._calls[1].id == "3-4" + def test_high_scoped_parametrize_reordering(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("arg2", [3, 4]) + @pytest.mark.parametrize("arg1", [0, 1, 2], scope='module') + def test1(arg1, arg2): + pass + + def test2(): + pass + + @pytest.mark.parametrize("arg1", [0, 1, 2], scope='module') + def test3(arg1): + pass + """ + ) + result = pytester.runpytest("--collect-only") + result.stdout.re_match_lines( + [ + r" <Function test1\[0-3\]>", + r" <Function test1\[0-4\]>", + r" <Function test3\[0\]>", + r" <Function test1\[1-3\]>", + r" <Function test1\[1-4\]>", + r" <Function test3\[1\]>", + r" <Function test1\[2-3\]>", + r" <Function test1\[2-4\]>", + r" <Function test3\[2\]>", + r" <Function test2>", + ] + ) + def test_parametrize_multiple_times(self, pytester: Pytester) -> None: pytester.makepyfile( """ @@ -969,27 +1090,6 @@ class TestMetafunc: """ ) - def test_format_args(self) -> None: - def function1(): - pass - - assert _format_args(function1) == "()" - - def function2(arg1): - pass - - assert _format_args(function2) == "(arg1)" - - def function3(arg1, arg2="qwe"): - pass - - assert _format_args(function3) == "(arg1, arg2='qwe')" - - def function4(arg1, *args, **kwargs): - pass - - assert _format_args(function4) == "(arg1, *args, **kwargs)" - class TestMetafuncFunctional: def test_attributes(self, pytester: Pytester) -> None: @@ -1272,7 +1372,7 @@ class TestMetafuncFunctional: """ import pytest - @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type)) + @pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError())) def test_ids_numbers(x,expected): assert x * 2 == expected """ @@ -1280,8 +1380,8 @@ class TestMetafuncFunctional: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "In test_ids_numbers: ids must be list of string/float/int/bool," - " found: <class 'type'> (type: <class 'type'>) at index 2" + "In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. " + "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__." ] ) @@ -1376,7 +1476,8 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_1" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("conftest.py").write_text( textwrap.dedent( @@ -1384,10 +1485,15 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_2" """ - ) + ), + encoding="utf-8", + ) + sub1.joinpath("test_in_sub1.py").write_text( + "def test_1(): pass", encoding="utf-8" + ) + sub2.joinpath("test_in_sub2.py").write_text( + "def test_2(): pass", encoding="utf-8" ) - sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1) result.assert_outcomes(passed=3) @@ -1420,7 +1526,7 @@ class TestMetafuncFunctional: pass """ ) - result = pytester.runpytest("--collectonly") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( [ "collected 0 items / 1 error", @@ -1435,6 +1541,115 @@ class TestMetafuncFunctional: ] ) + @pytest.mark.parametrize("scope", ["class", "package"]) + def test_parametrize_missing_scope_doesnt_crash( + self, pytester: Pytester, scope: str + ) -> None: + """Doesn't crash when parametrize(scope=<scope>) is used without a + corresponding <scope> node.""" + pytester.makepyfile( + f""" + import pytest + + @pytest.mark.parametrize("x", [0], scope="{scope}") + def test_it(x): pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_parametrize_module_level_test_with_class_scope( + self, pytester: Pytester + ) -> None: + """ + Test that a class-scoped parametrization without a corresponding `Class` + gets module scope, i.e. we only create a single FixtureDef for it per module. + """ + module = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("x", [0, 1], scope="class") + def test_1(x): + pass + + @pytest.mark.parametrize("x", [1, 2], scope="module") + def test_2(x): + pass + """ + ) + test_1_0, _, test_2_0, _ = pytester.genitems((pytester.getmodulecol(module),)) + + assert isinstance(test_1_0, Function) + assert test_1_0.name == "test_1[0]" + test_1_fixture_x = test_1_0._fixtureinfo.name2fixturedefs["x"][-1] + + assert isinstance(test_2_0, Function) + assert test_2_0.name == "test_2[1]" + test_2_fixture_x = test_2_0._fixtureinfo.name2fixturedefs["x"][-1] + + assert test_1_fixture_x is test_2_fixture_x + + def test_reordering_with_scopeless_and_just_indirect_parametrization( + self, pytester: Pytester + ) -> None: + pytester.makeconftest( + """ + import pytest + + @pytest.fixture(scope="package") + def fixture1(): + pass + """ + ) + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(scope="module") + def fixture0(): + pass + + @pytest.fixture(scope="module") + def fixture1(fixture0): + pass + + @pytest.mark.parametrize("fixture1", [0], indirect=True) + def test_0(fixture1): + pass + + @pytest.fixture(scope="module") + def fixture(): + pass + + @pytest.mark.parametrize("fixture", [0], indirect=True) + def test_1(fixture): + pass + + def test_2(): + pass + + class Test: + @pytest.fixture(scope="class") + def fixture(self, fixture): + pass + + @pytest.mark.parametrize("fixture", [0], indirect=True) + def test_3(self, fixture): + pass + """ + ) + result = pytester.runpytest("-v") + assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "*test_0*", + "*test_1*", + "*test_2*", + "*test_3*", + ] + ) + class TestMetafuncFunctionalAuto: """Tests related to automatically find out the correct scope for @@ -1725,7 +1940,7 @@ class TestMarkersWithParametrization: @pytest.mark.parametrize("strict", [True, False]) def test_xfail_passing_is_xpass(self, pytester: Pytester, strict: bool) -> None: - s = """ + s = f""" import pytest m = pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}) @@ -1737,9 +1952,7 @@ class TestMarkersWithParametrization: ]) def test_increment(n, expected): assert n + 1 == expected - """.format( - strict=strict - ) + """ pytester.makepyfile(s) reprec = pytester.inline_run() passed, failed = (2, 1) if strict else (3, 0) @@ -1790,7 +2003,7 @@ class TestMarkersWithParametrization: @pytest.mark.parametrize("strict", [True, False]) def test_parametrize_marked_value(self, pytester: Pytester, strict: bool) -> None: - s = """ + s = f""" import pytest @pytest.mark.parametrize(("n", "expected"), [ @@ -1805,9 +2018,7 @@ class TestMarkersWithParametrization: ]) def test_increment(n, expected): assert n + 1 == expected - """.format( - strict=strict - ) + """ pytester.makepyfile(s) reprec = pytester.inline_run() passed, failed = (0, 2) if strict else (2, 0) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/python/raises.py b/tests/wpt/tests/tools/third_party/pytest/testing/python/raises.py index 2d62e91091b..929865e31a0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/python/raises.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/python/raises.py @@ -1,9 +1,10 @@ +# mypy: allow-untyped-defs import re import sys -import pytest from _pytest.outcomes import Failed from _pytest.pytester import Pytester +import pytest class TestRaises: @@ -19,6 +20,16 @@ class TestRaises: excinfo = pytest.raises(ValueError, int, "hello") assert "invalid literal" in str(excinfo.value) + def test_raises_does_not_allow_none(self): + with pytest.raises(ValueError, match="Expected an exception type or"): + # We're testing that this invalid usage gives a helpful error, + # so we can ignore Mypy telling us that None is invalid. + pytest.raises(expected_exception=None) # type: ignore + + def test_raises_does_not_allow_empty_tuple(self): + with pytest.raises(ValueError, match="Expected an exception type or"): + pytest.raises(expected_exception=()) + def test_raises_callable_no_exception(self) -> None: class A: def __call__(self): @@ -82,13 +93,9 @@ class TestRaises: def test_does_not_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ - from contextlib import contextmanager + from contextlib import nullcontext as does_not_raise import pytest - @contextmanager - def does_not_raise(): - yield - @pytest.mark.parametrize('example_input,expectation', [ (3, does_not_raise()), (2, does_not_raise()), @@ -107,13 +114,9 @@ class TestRaises: def test_does_not_raise_does_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ - from contextlib import contextmanager + from contextlib import nullcontext as does_not_raise import pytest - @contextmanager - def does_not_raise(): - yield - @pytest.mark.parametrize('example_input,expectation', [ (0, does_not_raise()), (1, pytest.raises(ZeroDivisionError)), @@ -144,7 +147,7 @@ class TestRaises: try: pytest.raises(ValueError, int, "0") except pytest.fail.Exception as e: - assert e.msg == f"DID NOT RAISE {repr(ValueError)}" + assert e.msg == f"DID NOT RAISE {ValueError!r}" else: assert False, "Expected pytest.raises.Exception" @@ -152,7 +155,7 @@ class TestRaises: with pytest.raises(ValueError): pass except pytest.fail.Exception as e: - assert e.msg == f"DID NOT RAISE {repr(ValueError)}" + assert e.msg == f"DID NOT RAISE {ValueError!r}" else: assert False, "Expected pytest.raises.Exception" @@ -191,10 +194,12 @@ class TestRaises: int("asdf") msg = "with base 16" - expr = "Regex pattern {!r} does not match \"invalid literal for int() with base 10: 'asdf'\".".format( - msg + expr = ( + "Regex pattern did not match.\n" + f" Regex: {msg!r}\n" + " Input: \"invalid literal for int() with base 10: 'asdf'\"" ) - with pytest.raises(AssertionError, match=re.escape(expr)): + with pytest.raises(AssertionError, match="(?m)" + re.escape(expr)): with pytest.raises(ValueError, match=msg): int("asdf", base=10) @@ -217,7 +222,7 @@ class TestRaises: with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") (msg,) = excinfo.value.args - assert msg == 'Regex pattern "\'foo" does not match "\'bar".' + assert msg == '''Regex pattern did not match.\n Regex: "'foo"\n Input: "'bar"''' def test_match_failure_exact_string_message(self): message = "Oh here is a message with (42) numbers in parameters" @@ -226,9 +231,10 @@ class TestRaises: raise AssertionError(message) (msg,) = excinfo.value.args assert msg == ( - "Regex pattern 'Oh here is a message with (42) numbers in " - "parameters' does not match 'Oh here is a message with (42) " - "numbers in parameters'. Did you mean to `re.escape()` the regex?" + "Regex pattern did not match.\n" + " Regex: 'Oh here is a message with (42) numbers in parameters'\n" + " Input: 'Oh here is a message with (42) numbers in parameters'\n" + " Did you mean to `re.escape()` the regex?" ) def test_raises_match_wrong_type(self): @@ -274,7 +280,7 @@ class TestRaises: def test_raises_context_manager_with_kwargs(self): with pytest.raises(TypeError) as excinfo: - with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] + with pytest.raises(OSError, foo="bar"): # type: ignore[call-overload] pass assert "Unexpected keyword arguments" in str(excinfo.value) @@ -296,3 +302,16 @@ class TestRaises: with pytest.raises(("hello", NotAnException)): # type: ignore[arg-type] pass # pragma: no cover assert "must be a BaseException type, not str" in str(excinfo.value) + + def test_issue_11872(self) -> None: + """Regression test for #11872. + + urllib.error.HTTPError on Python<=3.9 raises KeyError instead of + AttributeError on invalid attribute access. + + https://github.com/python/cpython/issues/98778 + """ + from urllib.error import HTTPError + + with pytest.raises(HTTPError, match="Not Found"): + raise HTTPError(code=404, msg="Not Found", fp=None, hdrs=None, url="") # type: ignore [arg-type] diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_argcomplete.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_argcomplete.py index 8c10e230b0c..0c41c0286a4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_argcomplete.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_argcomplete.py @@ -1,9 +1,11 @@ +# mypy: allow-untyped-defs +from pathlib import Path import subprocess import sys -from pathlib import Path -import pytest from _pytest.monkeypatch import MonkeyPatch +import pytest + # Test for _argcomplete but not specific for any application. diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_assertion.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_assertion.py index 2516ff1629e..ef4e36644d9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_assertion.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_assertion.py @@ -1,32 +1,81 @@ -import collections +# mypy: allow-untyped-defs import sys import textwrap from typing import Any from typing import List from typing import MutableSequence +from typing import NamedTuple from typing import Optional import attr -import _pytest.assertion as plugin -import pytest from _pytest import outcomes +import _pytest.assertion as plugin from _pytest.assertion import truncate from _pytest.assertion import util +from _pytest.config import Config as _Config from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest -def mock_config(verbose=0): +def mock_config(verbose: int = 0, assertion_override: Optional[int] = None): + class TerminalWriter: + def _highlight(self, source, lexer="python"): + return source + class Config: - def getoption(self, name): - if name == "verbose": + def get_terminal_writer(self): + return TerminalWriter() + + def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: + if verbosity_type is None: + return verbose + if verbosity_type == _Config.VERBOSITY_ASSERTIONS: + if assertion_override is not None: + return assertion_override return verbose - raise KeyError("Not mocked out: %s" % name) + + raise KeyError(f"Not mocked out: {verbosity_type}") return Config() +class TestMockConfig: + SOME_VERBOSITY_LEVEL = 3 + SOME_OTHER_VERBOSITY_LEVEL = 10 + + def test_verbose_exposes_value(self): + config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL) + + assert config.get_verbosity() == TestMockConfig.SOME_VERBOSITY_LEVEL + + def test_get_assertion_override_not_set_verbose_value(self): + config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL) + + assert ( + config.get_verbosity(_Config.VERBOSITY_ASSERTIONS) + == TestMockConfig.SOME_VERBOSITY_LEVEL + ) + + def test_get_assertion_override_set_custom_value(self): + config = mock_config( + verbose=TestMockConfig.SOME_VERBOSITY_LEVEL, + assertion_override=TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL, + ) + + assert ( + config.get_verbosity(_Config.VERBOSITY_ASSERTIONS) + == TestMockConfig.SOME_OTHER_VERBOSITY_LEVEL + ) + + def test_get_unsupported_type_error(self): + config = mock_config(verbose=TestMockConfig.SOME_VERBOSITY_LEVEL) + + with pytest.raises(KeyError): + config.get_verbosity("--- NOT A VERBOSITY LEVEL ---") + + class TestImportHookInstallation: @pytest.mark.parametrize("initial_conftest", [True, False]) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) @@ -83,7 +132,7 @@ class TestImportHookInstallation: "E assert {'failed': 1,... 'skipped': 0} == {'failed': 0,... 'skipped': 0}", "E Omitting 1 identical items, use -vv to show", "E Differing items:", - "E Use -v to get the full diff", + "E Use -v to get more diff", ] ) # XXX: unstable output. @@ -133,11 +182,9 @@ class TestImportHookInstallation: """ plugins = '"ham"' if mode == "str" else '["ham"]' contents = { - "conftest.py": """ + "conftest.py": f""" pytest_plugins = {plugins} - """.format( - plugins=plugins - ), + """, "ham.py": """ import pytest """, @@ -199,8 +246,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ + import importlib.metadata import pytest - from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' @@ -220,7 +267,7 @@ class TestImportHookInstallation: def distributions(): return (DummyDistInfo(),) - importlib_metadata.distributions = distributions + importlib.metadata.distributions = distributions pytest.main() """, "test_foo.py": """\ @@ -344,6 +391,7 @@ class TestAssert_reprcompare: def test_text_diff(self) -> None: assert callequal("spam", "eggs") == [ "'spam' == 'eggs'", + "", "- eggs", "+ spam", ] @@ -351,7 +399,7 @@ class TestAssert_reprcompare: def test_text_skipping(self) -> None: lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs") assert lines is not None - assert "Skipping" in lines[1] + assert "Skipping" in lines[2] for line in lines: assert "a" * 50 not in line @@ -375,8 +423,9 @@ class TestAssert_reprcompare: assert diff == [ "b'spam' == b'eggs'", + "", "At index 0 diff: b's' != b'e'", - "Use -v to get the full diff", + "Use -v to get more diff", ] def test_bytes_diff_verbose(self) -> None: @@ -384,7 +433,9 @@ class TestAssert_reprcompare: diff = callequal(b"spam", b"eggs", verbose=1) assert diff == [ "b'spam' == b'eggs'", + "", "At index 0 diff: b's' != b'e'", + "", "Full diff:", "- b'eggs'", "+ b'spam'", @@ -403,11 +454,14 @@ class TestAssert_reprcompare: [0, 2], """ Full diff: - - [0, 2] + [ + 0, + - 2, ? ^ - + [0, 1] + + 1, ? ^ - """, + ] + """, id="lists", ), pytest.param( @@ -415,10 +469,12 @@ class TestAssert_reprcompare: {0: 2}, """ Full diff: - - {0: 2} - ? ^ - + {0: 1} - ? ^ + { + - 0: 2, + ? ^ + + 0: 1, + ? ^ + } """, id="dicts", ), @@ -427,10 +483,13 @@ class TestAssert_reprcompare: {0, 2}, """ Full diff: - - {0, 2} + { + 0, + - 2, ? ^ - + {0, 1} + + 1, ? ^ + } """, id="sets", ), @@ -444,11 +503,20 @@ class TestAssert_reprcompare: """ expl = callequal(left, right, verbose=0) assert expl is not None - assert expl[-1] == "Use -v to get the full diff" + assert expl[-1] == "Use -v to get more diff" verbose_expl = callequal(left, right, verbose=1) assert verbose_expl is not None assert "\n".join(verbose_expl).endswith(textwrap.dedent(expected).strip()) + def test_iterable_quiet(self) -> None: + expl = callequal([1, 2], [10, 2], verbose=-1) + assert expl == [ + "[1, 2] == [10, 2]", + "", + "At index 0 diff: 1 != 10", + "Use -v to get more diff", + ] + def test_iterable_full_diff_ci( self, monkeypatch: MonkeyPatch, pytester: Pytester ) -> None: @@ -466,7 +534,7 @@ class TestAssert_reprcompare: monkeypatch.delenv("CI", raising=False) result = pytester.runpytest() - result.stdout.fnmatch_lines(["E Use -v to get the full diff"]) + result.stdout.fnmatch_lines(["E Use -v to get more diff"]) def test_list_different_lengths(self) -> None: expl = callequal([0, 1], [0, 1, 2]) @@ -483,26 +551,30 @@ class TestAssert_reprcompare: diff = callequal(l1, l2, verbose=True) assert diff == [ "['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']", + "", "Right contains one more item: '" + long_d + "'", + "", "Full diff:", " [", - " 'a',", - " 'b',", - " 'c',", - "- '" + long_d + "',", + " 'a',", + " 'b',", + " 'c',", + "- '" + long_d + "',", " ]", ] diff = callequal(l2, l1, verbose=True) assert diff == [ "['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']", + "", "Left contains one more item: '" + long_d + "'", + "", "Full diff:", " [", - " 'a',", - " 'b',", - " 'c',", - "+ '" + long_d + "',", + " 'a',", + " 'b',", + " 'c',", + "+ '" + long_d + "',", " ]", ] @@ -515,36 +587,40 @@ class TestAssert_reprcompare: diff = callequal(l1, l2, verbose=True) assert diff == [ "['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']", + "", "At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'", + "", "Full diff:", " [", - "+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", - " 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',", - " 'cccccccccccccccccccccccccccccc',", - "- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", + "+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", + " 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',", + " 'cccccccccccccccccccccccccccccc',", + "- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", " ]", ] def test_list_dont_wrap_strings(self) -> None: long_a = "a" * 10 - l1 = ["a"] + [long_a for _ in range(0, 7)] + l1 = ["a"] + [long_a for _ in range(7)] l2 = ["should not get wrapped"] diff = callequal(l1, l2, verbose=True) assert diff == [ "['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']", + "", "At index 0 diff: 'a' != 'should not get wrapped'", "Left contains 7 more items, first extra item: 'aaaaaaaaaa'", + "", "Full diff:", " [", - "- 'should not get wrapped',", - "+ 'a',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", - "+ 'aaaaaaaaaa',", + "- 'should not get wrapped',", + "+ 'a',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", + "+ 'aaaaaaaaaa',", " ]", ] @@ -555,31 +631,45 @@ class TestAssert_reprcompare: diff = callequal(d1, d2, verbose=True) assert diff == [ "{'common': 1,...1, 'env2': 2}} == {'common': 1,...: {'env1': 1}}", + "", "Omitting 1 identical items, use -vv to show", "Differing items:", "{'env': {'env1': 1, 'env2': 2}} != {'env': {'env1': 1}}", + "", "Full diff:", - "- {'common': 1, 'env': {'env1': 1}}", - "+ {'common': 1, 'env': {'env1': 1, 'env2': 2}}", - "? +++++++++++", + " {", + " 'common': 1,", + " 'env': {", + " 'env1': 1,", + "+ 'env2': 2,", + " },", + " }", ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 3}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) assert diff == [ "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}", + "", "Omitting 1 identical items, use -vv to show", "Right contains 1 more item:", "{'new': 1}", + "", "Full diff:", " {", - " 'env': {'sub': {'long_a': '" + long_a + "',", - " 'sub1': {'long_a': 'substring that gets wrapped substring '", - " 'that gets wrapped '}}},", - "- 'new': 1,", + " 'env': {", + " 'sub': {", + f" 'long_a': '{long_a}',", + " 'sub1': {", + " 'long_a': 'substring that gets wrapped substring that gets wrapped '", + " 'substring that gets wrapped ',", + " },", + " },", + " },", + "- 'new': 1,", " }", ] @@ -591,7 +681,7 @@ class TestAssert_reprcompare: def test_dict_omitting(self) -> None: lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}) assert lines is not None - assert lines[1].startswith("Omitting 1 identical item") + assert lines[2].startswith("Omitting 1 identical item") assert "Common items" not in lines for line in lines[1:]: assert "b" not in line @@ -600,60 +690,109 @@ class TestAssert_reprcompare: """Ensure differing items are visible for verbosity=1 (#1512).""" lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=1) assert lines is not None - assert lines[1].startswith("Omitting 1 identical item") - assert lines[2].startswith("Differing items") - assert lines[3] == "{'a': 0} != {'a': 1}" + assert lines[1] == "" + assert lines[2].startswith("Omitting 1 identical item") + assert lines[3].startswith("Differing items") + assert lines[4] == "{'a': 0} != {'a': 1}" assert "Common items" not in lines def test_dict_omitting_with_verbosity_2(self) -> None: lines = callequal({"a": 0, "b": 1}, {"a": 1, "b": 1}, verbose=2) assert lines is not None - assert lines[1].startswith("Common items:") - assert "Omitting" not in lines[1] - assert lines[2] == "{'b': 1}" + assert lines[2].startswith("Common items:") + assert "Omitting" not in lines[2] + assert lines[3] == "{'b': 1}" def test_dict_different_items(self) -> None: lines = callequal({"a": 0}, {"b": 1, "c": 2}, verbose=2) assert lines == [ "{'a': 0} == {'b': 1, 'c': 2}", + "", "Left contains 1 more item:", "{'a': 0}", "Right contains 2 more items:", "{'b': 1, 'c': 2}", + "", "Full diff:", - "- {'b': 1, 'c': 2}", - "+ {'a': 0}", + " {", + "- 'b': 1,", + "? ^ ^", + "+ 'a': 0,", + "? ^ ^", + "- 'c': 2,", + " }", ] lines = callequal({"b": 1, "c": 2}, {"a": 0}, verbose=2) assert lines == [ "{'b': 1, 'c': 2} == {'a': 0}", + "", "Left contains 2 more items:", "{'b': 1, 'c': 2}", "Right contains 1 more item:", "{'a': 0}", + "", "Full diff:", - "- {'a': 0}", - "+ {'b': 1, 'c': 2}", + " {", + "- 'a': 0,", + "? ^ ^", + "+ 'b': 1,", + "? ^ ^", + "+ 'c': 2,", + " }", ] def test_sequence_different_items(self) -> None: lines = callequal((1, 2), (3, 4, 5), verbose=2) assert lines == [ "(1, 2) == (3, 4, 5)", + "", "At index 0 diff: 1 != 3", "Right contains one more item: 5", + "", "Full diff:", - "- (3, 4, 5)", - "+ (1, 2)", + " (", + "- 3,", + "? ^", + "+ 1,", + "? ^", + "- 4,", + "? ^", + "+ 2,", + "? ^", + "- 5,", + " )", ] lines = callequal((1, 2, 3), (4,), verbose=2) assert lines == [ "(1, 2, 3) == (4,)", + "", "At index 0 diff: 1 != 4", "Left contains 2 more items, first extra item: 2", + "", "Full diff:", - "- (4,)", - "+ (1, 2, 3)", + " (", + "- 4,", + "? ^", + "+ 1,", + "? ^", + "+ 2,", + "+ 3,", + " )", + ] + lines = callequal((1, 2, 3), (1, 20, 3), verbose=2) + assert lines == [ + "(1, 2, 3) == (1, 20, 3)", + "", + "At index 1 diff: 2 != 20", + "", + "Full diff:", + " (", + " 1,", + "- 20,", + "? -", + "+ 2,", + " 3,", + " )", ] def test_set(self) -> None: @@ -699,32 +838,6 @@ class TestAssert_reprcompare: assert expl is not None assert len(expl) > 1 - def test_repr_verbose(self) -> None: - class Nums: - def __init__(self, nums): - self.nums = nums - - def __repr__(self): - return str(self.nums) - - list_x = list(range(5000)) - list_y = list(range(5000)) - list_y[len(list_y) // 2] = 3 - nums_x = Nums(list_x) - nums_y = Nums(list_y) - - assert callequal(nums_x, nums_y) is None - - expl = callequal(nums_x, nums_y, verbose=1) - assert expl is not None - assert "+" + repr(nums_x) in expl - assert "-" + repr(nums_y) in expl - - expl = callequal(nums_x, nums_y, verbose=2) - assert expl is not None - assert "+" + repr(nums_x) in expl - assert "-" + repr(nums_y) in expl - def test_list_bad_repr(self) -> None: class A: def __repr__(self): @@ -737,11 +850,9 @@ class TestAssert_reprcompare: assert expl is not None assert expl[0].startswith("{} == <[ValueError") assert "raised in repr" in expl[0] - assert expl[1:] == [ + assert expl[2:] == [ "(pytest_assertion plugin: representation of details failed:" - " {}:{}: ValueError: 42.".format( - __file__, A.__repr__.__code__.co_firstlineno + 1 - ), + f" {__file__}:{A.__repr__.__code__.co_firstlineno + 1}: ValueError: 42.", " Probably an object has a faulty __repr__.)", ] @@ -763,6 +874,7 @@ class TestAssert_reprcompare: def test_unicode(self) -> None: assert callequal("£€", "£") == [ "'£€' == '£'", + "", "- £", "+ £€", ] @@ -778,7 +890,7 @@ class TestAssert_reprcompare: return "\xff" expl = callequal(A(), "1") - assert expl == ["ÿ == '1'", "- 1"] + assert expl == ["ÿ == '1'", "", "- 1"] def test_format_nonascii_explanation(self) -> None: assert util.format_explanation("λ") @@ -794,9 +906,28 @@ class TestAssert_reprcompare: msg = "\n".join(expl) assert msg + def test_nfc_nfd_same_string(self) -> None: + # issue 3426 + left = "hyv\xe4" + right = "hyva\u0308" + expl = callequal(left, right) + assert expl == [ + r"'hyv\xe4' == 'hyva\u0308'", + "", + f"- {right!s}", + f"+ {left!s}", + ] + + expl = callequal(left, right, verbose=2) + assert expl == [ + r"'hyv\xe4' == 'hyva\u0308'", + "", + f"- {right!s}", + f"+ {left!s}", + ] + class TestAssert_reprcompare_dataclass: - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses(self, pytester: Pytester) -> None: p = pytester.copy_example("dataclasses/test_compare_dataclasses.py") result = pytester.runpytest(p) @@ -808,14 +939,13 @@ class TestAssert_reprcompare_dataclass: "E ['field_b']", "E ", "E Drill down into differing attribute field_b:", - "E field_b: 'b' != 'c'...", - "E ", - "E ...Full output truncated (3 lines hidden), use '-vv' to show", + "E field_b: 'b' != 'c'", + "E - c", + "E + b", ], consecutive=True, ) - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_recursive_dataclasses(self, pytester: Pytester) -> None: p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p) @@ -829,12 +959,11 @@ class TestAssert_reprcompare_dataclass: "E Drill down into differing attribute g:", "E g: S(a=10, b='ten') != S(a=20, b='xxx')...", "E ", - "E ...Full output truncated (52 lines hidden), use '-vv' to show", + "E ...Full output truncated (51 lines hidden), use '-vv' to show", ], consecutive=True, ) - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_recursive_dataclasses_verbose(self, pytester: Pytester) -> None: p = pytester.copy_example("dataclasses/test_compare_recursive_dataclasses.py") result = pytester.runpytest(p, "-vv") @@ -854,8 +983,6 @@ class TestAssert_reprcompare_dataclass: "E ", "E Drill down into differing attribute a:", "E a: 10 != 20", - "E +10", - "E -20", "E ", "E Drill down into differing attribute b:", "E b: 'ten' != 'xxx'", @@ -867,7 +994,6 @@ class TestAssert_reprcompare_dataclass: consecutive=True, ) - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses_verbose(self, pytester: Pytester) -> None: p = pytester.copy_example("dataclasses/test_compare_dataclasses_verbose.py") result = pytester.runpytest(p, "-vv") @@ -881,7 +1007,6 @@ class TestAssert_reprcompare_dataclass: ] ) - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_dataclasses_with_attribute_comparison_off( self, pytester: Pytester ) -> None: @@ -891,7 +1016,6 @@ class TestAssert_reprcompare_dataclass: result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=0, passed=1) - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") def test_comparing_two_different_data_classes(self, pytester: Pytester) -> None: p = pytester.copy_example( "dataclasses/test_compare_two_different_dataclasses.py" @@ -899,6 +1023,22 @@ class TestAssert_reprcompare_dataclass: result = pytester.runpytest(p, "-vv") result.assert_outcomes(failed=0, passed=1) + def test_data_classes_with_custom_eq(self, pytester: Pytester) -> None: + p = pytester.copy_example( + "dataclasses/test_compare_dataclasses_with_custom_eq.py" + ) + # issue 9362 + result = pytester.runpytest(p, "-vv") + result.assert_outcomes(failed=1, passed=0) + result.stdout.no_re_match_line(".*Differing attributes.*") + + def test_data_classes_with_initvar(self, pytester: Pytester) -> None: + p = pytester.copy_example("dataclasses/test_compare_initvar.py") + # issue 9820 + result = pytester.runpytest(p, "-vv") + result.assert_outcomes(failed=1, passed=0) + result.stdout.no_re_match_line(".*AttributeError.*") + class TestAssert_reprcompare_attrsclass: def test_attrs(self) -> None: @@ -982,7 +1122,6 @@ class TestAssert_reprcompare_attrsclass: right = SimpleDataObject(1, "b") lines = callequal(left, right, verbose=2) - print(lines) assert lines is not None assert lines[2].startswith("Matching attributes:") assert "Omitting" not in lines[1] @@ -1007,10 +1146,42 @@ class TestAssert_reprcompare_attrsclass: lines = callequal(left, right) assert lines is None + def test_attrs_with_auto_detect_and_custom_eq(self) -> None: + @attr.s( + auto_detect=True + ) # attr.s doesn't ignore a custom eq if auto_detect=True + class SimpleDataObject: + field_a = attr.ib() + + def __eq__(self, other): # pragma: no cover + return super().__eq__(other) + + left = SimpleDataObject(1) + right = SimpleDataObject(2) + # issue 9362 + lines = callequal(left, right, verbose=2) + assert lines is None + + def test_attrs_with_custom_eq(self) -> None: + @attr.define(slots=False) + class SimpleDataObject: + field_a = attr.ib() + + def __eq__(self, other): # pragma: no cover + return super().__eq__(other) + + left = SimpleDataObject(1) + right = SimpleDataObject(2) + # issue 9362 + lines = callequal(left, right, verbose=2) + assert lines is None + class TestAssert_reprcompare_namedtuple: def test_namedtuple(self) -> None: - NT = collections.namedtuple("NT", ["a", "b"]) + class NT(NamedTuple): + a: Any + b: Any left = NT(1, "b") right = NT(1, "c") @@ -1027,12 +1198,17 @@ class TestAssert_reprcompare_namedtuple: " b: 'b' != 'c'", " - c", " + b", - "Use -v to get the full diff", + "Use -v to get more diff", ] def test_comparing_two_different_namedtuple(self) -> None: - NT1 = collections.namedtuple("NT1", ["a", "b"]) - NT2 = collections.namedtuple("NT2", ["a", "b"]) + class NT1(NamedTuple): + a: Any + b: Any + + class NT2(NamedTuple): + a: Any + b: Any left = NT1(1, "b") right = NT2(2, "b") @@ -1041,8 +1217,9 @@ class TestAssert_reprcompare_namedtuple: # Because the types are different, uses the generic sequence matcher. assert lines == [ "NT1(a=1, b='b') == NT2(a=2, b='b')", + "", "At index 0 diff: 1 != 2", - "Use -v to get the full diff", + "Use -v to get more diff", ] @@ -1151,30 +1328,55 @@ class TestTruncateExplanation: def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: expl = ["" for x in range(50)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) + assert len(result) != len(expl) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] - assert "43 lines hidden" in result[-1] + assert "42 lines hidden" in result[-1] last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: - expl = ["a" for x in range(100)] + total_lines = 100 + expl = ["a" for x in range(total_lines)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] - assert "93 lines hidden" in result[-1] + assert f"{total_lines - 8} lines hidden" in result[-1] last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") + def test_truncates_at_8_lines_when_there_is_one_line_to_remove(self) -> None: + """The number of line in the result is 9, the same number as if we truncated.""" + expl = ["a" for x in range(9)] + result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) + assert result == expl + assert "truncated" not in result[-1] + + def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_chars( + self, + ) -> None: + line = "a" * 10 + expl = [line, line] + result = truncate._truncate_explanation(expl, max_lines=10, max_chars=10) + assert result == [line, line] + + def test_truncates_edgecase_when_truncation_message_makes_the_result_longer_for_lines( + self, + ) -> None: + line = "a" * 10 + expl = [line, line] + result = truncate._truncate_explanation(expl, max_lines=1, max_chars=100) + assert result == [line, line] + def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: - expl = ["a" * 80 for x in range(16)] + expl = [chr(97 + x) * 80 for x in range(16)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl - assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG + assert len(result) == 16 - 8 + self.LINES_IN_TRUNCATION_MSG assert "Full output truncated" in result[-1] - assert "9 lines hidden" in result[-1] + assert "8 lines hidden" in result[-1] last_line_before_trunc_msg = result[-self.LINES_IN_TRUNCATION_MSG - 1] assert last_line_before_trunc_msg.endswith("...") @@ -1200,7 +1402,6 @@ class TestTruncateExplanation: def test_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None: """Test against full runpytest() output.""" - line_count = 7 line_len = 100 expected_truncated_lines = 2 @@ -1223,7 +1424,6 @@ class TestTruncateExplanation: [ "*+ 1*", "*+ 3*", - "*+ 5*", "*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines, ] ) @@ -1267,6 +1467,7 @@ def test_rewritten(pytester: Pytester) -> None: def test_reprcompare_notin() -> None: assert callop("not in", "foo", "aaafoobbb") == [ "'foo' not in 'aaafoobbb'", + "", "'foo' is contained here:", " aaafoobbb", "? +++", @@ -1276,6 +1477,7 @@ def test_reprcompare_notin() -> None: def test_reprcompare_whitespaces() -> None: assert callequal("\r\n", "\n") == [ r"'\r\n' == '\n'", + "", r"Strings contain only whitespace, escaping them using repr()", r"- '\n'", r"+ '\r\n'", @@ -1283,72 +1485,104 @@ def test_reprcompare_whitespaces() -> None: ] -def test_pytest_assertrepr_compare_integration(pytester: Pytester) -> None: - pytester.makepyfile( +class TestSetAssertions: + @pytest.mark.parametrize("op", [">=", ">", "<=", "<", "=="]) + def test_set_extra_item(self, op, pytester: Pytester) -> None: + pytester.makepyfile( + f""" + def test_hello(): + x = set("hello x") + y = set("hello y") + assert x {op} y """ - def test_hello(): - x = set(range(100)) - y = x.copy() - y.remove(50) - assert x == y - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*50*", - "*= 1 failed in*", - ] - ) + ) + + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + f"*assert x {op} y*", + ] + ) + if op in [">=", ">", "=="]: + result.stdout.fnmatch_lines( + [ + "*E*Extra items in the right set:*", + "*E*'y'", + ] + ) + if op in ["<=", "<", "=="]: + result.stdout.fnmatch_lines( + [ + "*E*Extra items in the left set:*", + "*E*'x'", + ] + ) + + @pytest.mark.parametrize("op", [">", "<", "!="]) + def test_set_proper_superset_equal(self, pytester: Pytester, op) -> None: + pytester.makepyfile( + f""" + def test_hello(): + x = set([1, 2, 3]) + y = x.copy() + assert x {op} y + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + f"*assert x {op} y*", + "*E*Both sets are equal*", + ] + ) -def test_sequence_comparison_uses_repr(pytester: Pytester) -> None: - pytester.makepyfile( + def test_pytest_assertrepr_compare_integration(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y """ - def test_hello(): - x = set("hello x") - y = set("hello y") - assert x == y - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*'x'*", - "*E*Extra items*right*", - "*E*'y'*", - ] - ) + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + "*= 1 failed in*", + ] + ) def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2") + a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( - 'def pytest_assertrepr_compare(): return ["summary a"]' + 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2") + b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( - 'def pytest_assertrepr_compare(): return ["summary b"]' + 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*def test_base():*", - "*E*assert 1 == 2*", "*def test_a():*", "*E*assert summary a*", "*def test_b():*", "*E*assert summary b*", + "*def test_base():*", + "*E*assert 1 == 2*", ] ) @@ -1513,9 +1747,9 @@ def test_recursion_source_decode(pytester: Pytester) -> None: ) result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( - """ - <Module*> - """ + [ + " <Module*>", + ] ) @@ -1627,15 +1861,7 @@ def test_raise_assertion_error_raising_repr(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - if sys.version_info >= (3, 11): - # python 3.11 has native support for un-str-able exceptions - result.stdout.fnmatch_lines( - ["E AssertionError: <exception str() failed>"] - ) - else: - result.stdout.fnmatch_lines( - ["E AssertionError: <unprintable AssertionError object>"] - ) + result.stdout.fnmatch_lines(["E AssertionError: <exception str() failed>"]) def test_issue_1944(pytester: Pytester) -> None: @@ -1683,3 +1909,139 @@ def test_assertion_location_with_coverage(pytester: Pytester) -> None: "*= 1 failed in*", ] ) + + +def test_reprcompare_verbose_long() -> None: + a = {f"v{i}": i for i in range(11)} + b = a.copy() + b["v2"] += 10 + lines = callop("==", a, b, verbose=2) + assert lines is not None + assert lines[0] == ( + "{'v0': 0, 'v1': 1, 'v2': 2, 'v3': 3, 'v4': 4, 'v5': 5, " + "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}" + " == " + "{'v0': 0, 'v1': 1, 'v2': 12, 'v3': 3, 'v4': 4, 'v5': 5, " + "'v6': 6, 'v7': 7, 'v8': 8, 'v9': 9, 'v10': 10}" + ) + + +@pytest.mark.parametrize("enable_colors", [True, False]) +@pytest.mark.parametrize( + ("test_code", "expected_lines"), + ( + ( + """ + def test(): + assert [0, 1] == [0, 2] + """, + [ + "{bold}{red}E At index 1 diff: {reset}{number}1{hl-reset}{endline} != {reset}{number}2*", + "{bold}{red}E {light-red}- 2,{hl-reset}{endline}{reset}", + "{bold}{red}E {light-green}+ 1,{hl-reset}{endline}{reset}", + ], + ), + ( + """ + def test(): + assert {f"number-is-{i}": i for i in range(1, 6)} == { + f"number-is-{i}": i for i in range(5) + } + """, + [ + "{bold}{red}E Common items:{reset}", + "{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-1{hl-reset}{str}'{hl-reset}: {number}1*", + "{bold}{red}E Left contains 1 more item:{reset}", + "{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-5{hl-reset}{str}'{hl-reset}: {number}5*", + "{bold}{red}E Right contains 1 more item:{reset}", + "{bold}{red}E {reset}{{{str}'{hl-reset}{str}number-is-0{hl-reset}{str}'{hl-reset}: {number}0*", + "{bold}{red}E {reset}{light-gray} {hl-reset} {{{endline}{reset}", + "{bold}{red}E {light-gray} {hl-reset} 'number-is-1': 1,{endline}{reset}", + "{bold}{red}E {light-green}+ 'number-is-5': 5,{hl-reset}{endline}{reset}", + ], + ), + ), +) +def test_comparisons_handle_colors( + pytester: Pytester, color_mapping, enable_colors, test_code, expected_lines +) -> None: + p = pytester.makepyfile(test_code) + result = pytester.runpytest( + f"--color={'yes' if enable_colors else 'no'}", "-vv", str(p) + ) + formatter = ( + color_mapping.format_for_fnmatch + if enable_colors + else color_mapping.strip_colors + ) + + result.stdout.fnmatch_lines(formatter(expected_lines), consecutive=False) + + +def test_fine_grained_assertion_verbosity(pytester: Pytester): + long_text = "Lorem ipsum dolor sit amet " * 10 + p = pytester.makepyfile( + f""" + def test_ok(): + pass + + + def test_words_fail(): + fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"] + fruits2 = ["banana", "apple", "orange", "melon", "kiwi"] + assert fruits1 == fruits2 + + + def test_numbers_fail(): + number_to_text1 = {{str(x): x for x in range(5)}} + number_to_text2 = {{str(x * 10): x * 10 for x in range(5)}} + assert number_to_text1 == number_to_text2 + + + def test_long_text_fail(): + long_text = "{long_text}" + assert "hello world" in long_text + """ + ) + pytester.makeini( + """ + [pytest] + verbosity_assertions = 2 + """ + ) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + f"{p.name} .FFF [100%]", + "E At index 2 diff: 'grapes' != 'orange'", + "E Full diff:", + "E [", + "E 'banana',", + "E 'apple',", + "E - 'orange',", + "E ? ^ ^^", + "E + 'grapes',", + "E ? ^ ^ +", + "E 'melon',", + "E 'kiwi',", + "E ]", + "E Full diff:", + "E {", + "E '0': 0,", + "E - '10': 10,", + "E ? - -", + "E + '1': 1,", + "E - '20': 20,", + "E ? - -", + "E + '2': 2,", + "E - '30': 30,", + "E ? - -", + "E + '3': 3,", + "E - '40': 40,", + "E ? - -", + "E + '4': 4,", + "E }", + f"E AssertionError: assert 'hello world' in '{long_text}'", + ] + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_assertrewrite.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_assertrewrite.py index 4417eb4350f..ac93c57dbd7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_assertrewrite.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_assertrewrite.py @@ -1,25 +1,27 @@ +# mypy: allow-untyped-defs import ast import errno +from functools import partial import glob import importlib import marshal import os +from pathlib import Path import py_compile import stat import sys import textwrap -import zipfile -from functools import partial -from pathlib import Path from typing import cast from typing import Dict +from typing import Generator from typing import List from typing import Mapping from typing import Optional from typing import Set +from unittest import mock +import zipfile import _pytest._code -import pytest from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest.assertion import util from _pytest.assertion.rewrite import _get_assertion_exprs @@ -33,6 +35,7 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.pathlib import make_numbered_dir from _pytest.pytester import Pytester +import pytest def rewrite(src: str) -> ast.Module: @@ -129,9 +132,8 @@ class TestAssertionRewrite: for n in [node, *ast.iter_child_nodes(node)]: assert n.lineno == 3 assert n.col_offset == 0 - if sys.version_info >= (3, 8): - assert n.end_lineno == 6 - assert n.end_col_offset == 3 + assert n.end_lineno == 6 + assert n.end_col_offset == 3 def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -158,7 +160,8 @@ class TestAssertionRewrite: "def special_asserter():\n" " def special_assert(x, y):\n" " assert x == y\n" - " return special_assert\n" + " return special_assert\n", + encoding="utf-8", ) pytester.makeconftest('pytest_plugins = ["plugin"]') pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") @@ -171,7 +174,9 @@ class TestAssertionRewrite: pytester.makepyfile(test_y="x = 1") xdir = pytester.mkdir("x") pytester.mkpydir(str(xdir.joinpath("test_Y"))) - xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2") + xdir.joinpath("test_Y").joinpath("__init__.py").write_text( + "x = 2", encoding="utf-8" + ) pytester.makepyfile( "import test_y\n" "import test_Y\n" @@ -195,23 +200,15 @@ class TestAssertionRewrite: assert getmsg(f2) == "assert False" def f3() -> None: - assert a_global # type: ignore[name-defined] # noqa + assert a_global # type: ignore[name-defined] # noqa: F821 assert getmsg(f3, {"a_global": False}) == "assert False" def f4() -> None: assert sys == 42 # type: ignore[comparison-overlap] - verbose = request.config.getoption("verbose") msg = getmsg(f4, {"sys": sys}) - if verbose > 0: - assert msg == ( - "assert <module 'sys' (built-in)> == 42\n" - " +<module 'sys' (built-in)>\n" - " -42" - ) - else: - assert msg == "assert sys == 42" + assert msg == "assert sys == 42" def f5() -> None: assert cls == 42 # type: ignore[name-defined] # noqa: F821 @@ -222,20 +219,7 @@ class TestAssertionRewrite: msg = getmsg(f5, {"cls": X}) assert msg is not None lines = msg.splitlines() - if verbose > 1: - assert lines == [ - f"assert {X!r} == 42", - f" +{X!r}", - " -42", - ] - elif verbose > 0: - assert lines == [ - "assert <class 'test_...e.<locals>.X'> == 42", - f" +{X!r}", - " -42", - ] - else: - assert lines == ["assert cls == 42"] + assert lines == ["assert cls == 42"] def test_assertrepr_compare_same_width(self, request) -> None: """Should use same width/truncation with same initial width.""" @@ -277,14 +261,11 @@ class TestAssertionRewrite: msg = getmsg(f, {"cls": Y}) assert msg is not None lines = msg.splitlines() - if request.config.getoption("verbose") > 0: - assert lines == ["assert 3 == 2", " +3", " -2"] - else: - assert lines == [ - "assert 3 == 2", - " + where 3 = Y.foo", - " + where Y = cls()", - ] + assert lines == [ + "assert 3 == 2", + " + where 3 = Y.foo", + " + where Y = cls()", + ] def test_assert_already_has_message(self) -> None: def f(): @@ -448,7 +429,7 @@ class TestAssertionRewrite: def f2() -> None: x = 1 - assert x == 1 or x == 2 + assert x == 1 or x == 2 # noqa: PLR1714 getmsg(f2, must_pass=True) @@ -661,10 +642,7 @@ class TestAssertionRewrite: assert len(values) == 11 msg = getmsg(f) - if request.config.getoption("verbose") > 0: - assert msg == "assert 10 == 11\n +10\n -11" - else: - assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])" + assert msg == "assert 10 == 11\n + where 10 = len([0, 1, 2, 3, 4, 5, ...])" def test_custom_reprcompare(self, monkeypatch) -> None: def my_reprcompare1(op, left, right) -> str: @@ -708,6 +686,25 @@ class TestAssertionRewrite: assert msg is not None assert "<MY42 object> < 0" in msg + def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + class A: + def __iter__(self): + raise ValueError() + + def __eq__(self, o: object) -> bool: + return self is o + + def __repr__(self): + return "<A object>" + + assert A() == A() + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"]) + def test_formatchar(self) -> None: def f() -> None: assert "%test" == "test" # type: ignore[comparison-overlap] @@ -730,10 +727,7 @@ class TestAssertionRewrite: msg = getmsg(f) assert msg is not None lines = util._format_lines([msg]) - if request.config.getoption("verbose") > 0: - assert lines == ["assert 0 == 1\n +0\n -1"] - else: - assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] + assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] def test_custom_repr_non_ascii(self) -> None: def f() -> None: @@ -754,7 +748,7 @@ class TestAssertionRewrite: class TestRewriteOnImport: def test_pycache_is_a_file(self, pytester: Pytester) -> None: - pytester.path.joinpath("__pycache__").write_text("Hello") + pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8") pytester.makepyfile( """ def test_rewritten(): @@ -787,11 +781,10 @@ class TestRewriteOnImport: f.close() z.chmod(256) pytester.makepyfile( - """ + f""" import sys - sys.path.append(%r) + sys.path.append({z_fn!r}) import test_gum.test_lizard""" - % (z_fn,) ) assert pytester.runpytest().ret == ExitCode.NO_TESTS_COLLECTED @@ -902,7 +895,11 @@ def test_rewritten(): ) @pytest.mark.skipif('"__pypy__" in sys.modules') - def test_pyc_vs_pyo(self, pytester: Pytester, monkeypatch) -> None: + def test_pyc_vs_pyo( + self, + pytester: Pytester, + monkeypatch: pytest.MonkeyPatch, + ) -> None: pytester.makepyfile( """ import pytest @@ -912,13 +909,13 @@ def test_rewritten(): ) p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-") tmp = "--basetemp=%s" % p - monkeypatch.setenv("PYTHONOPTIMIZE", "2") - monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) - monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - assert pytester.runpytest_subprocess(tmp).ret == 0 - tagged = "test_pyc_vs_pyo." + PYTEST_TAG - assert tagged + ".pyo" in os.listdir("__pycache__") - monkeypatch.undo() + with monkeypatch.context() as mp: + mp.setenv("PYTHONOPTIMIZE", "2") + mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False) + mp.delenv("PYTHONPYCACHEPREFIX", raising=False) + assert pytester.runpytest_subprocess(tmp).ret == 0 + tagged = "test_pyc_vs_pyo." + PYTEST_TAG + assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) assert pytester.runpytest_subprocess(tmp).ret == 1 @@ -931,7 +928,8 @@ def test_rewritten(): pkg.joinpath("test_blah.py").write_text( """ def test_rewritten(): - assert "@py_builtins" in globals()""" + assert "@py_builtins" in globals()""", + encoding="utf-8", ) assert pytester.runpytest().ret == 0 @@ -1037,9 +1035,9 @@ class TestAssertionRewriteHookDetails: ) assert pytester.runpytest().ret == 0 - def test_write_pyc(self, pytester: Pytester, tmp_path, monkeypatch) -> None: - from _pytest.assertion.rewrite import _write_pyc + def test_write_pyc(self, pytester: Pytester, tmp_path) -> None: from _pytest.assertion import AssertionState + from _pytest.assertion.rewrite import _write_pyc config = pytester.parseconfig() state = AssertionState(config, "rewrite") @@ -1049,27 +1047,8 @@ class TestAssertionRewriteHookDetails: co = compile("1", "f.py", "single") assert _write_pyc(state, co, os.stat(source_path), pycpath) - if sys.platform == "win32": - from contextlib import contextmanager - - @contextmanager - def atomic_write_failed(fn, mode="r", overwrite=False): - e = OSError() - e.errno = 10 - raise e - yield # type:ignore[unreachable] - - monkeypatch.setattr( - _pytest.assertion.rewrite, "atomic_write", atomic_write_failed - ) - else: - - def raise_oserror(*args): - raise OSError() - - monkeypatch.setattr("os.rename", raise_oserror) - - assert not _write_pyc(state, co, os.stat(source_path), pycpath) + with mock.patch.object(os, "replace", side_effect=OSError): + assert not _write_pyc(state, co, os.stat(source_path), pycpath) def test_resources_provider_for_loader(self, pytester: Pytester) -> None: """ @@ -1108,12 +1087,13 @@ class TestAssertionRewriteHookDetails: an exception that is propagated to the caller. """ import py_compile + from _pytest.assertion.rewrite import _read_pyc source = tmp_path / "source.py" pyc = Path(str(source) + "c") - source.write_text("def test(): pass") + source.write_text("def test(): pass", encoding="utf-8") py_compile.compile(str(source), str(pyc)) contents = pyc.read_bytes() @@ -1139,15 +1119,12 @@ class TestAssertionRewriteHookDetails: fn = tmp_path / "source.py" pyc = Path(str(fn) + "c") - fn.write_text("def test(): assert True") + fn.write_text("def test(): assert True", encoding="utf-8") source_stat, co = _rewrite_test(fn, config) _write_pyc(state, co, source_stat, pyc) assert _read_pyc(fn, pyc, state.trace) is not None - @pytest.mark.skipif( - sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity" - ) def test_read_pyc_more_invalid(self, tmp_path: Path) -> None: from _pytest.assertion.rewrite import _read_pyc @@ -1207,7 +1184,7 @@ class TestAssertionRewriteHookDetails: return False def rewrite_self(): - with open(__file__, 'w') as self: + with open(__file__, 'w', encoding='utf-8') as self: self.write('def reloaded(): return True') """, test_fun=""" @@ -1237,9 +1214,10 @@ class TestAssertionRewriteHookDetails: data = pkgutil.get_data('foo.test_foo', 'data.txt') assert data == b'Hey' """ - ) + ), + encoding="utf-8", ) - path.joinpath("data.txt").write_text("Hey") + path.joinpath("data.txt").write_text("Hey", encoding="utf-8") result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -1315,8 +1293,284 @@ class TestIssue2121: result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) +class TestIssue10743: + def test_assertion_walrus_operator(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def my_func(before, after): + return before == after + + def change_value(value): + return value.lower() + + def test_walrus_conversion(): + a = "Hello" + assert not my_func(a, a := change_value(a)) + assert a == "hello" + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_dont_rewrite(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + 'PYTEST_DONT_REWRITE' + def my_func(before, after): + return before == after + + def change_value(value): + return value.lower() + + def test_walrus_conversion_dont_rewrite(): + a = "Hello" + assert not my_func(a, a := change_value(a)) + assert a == "hello" + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_inline_walrus_operator(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def my_func(before, after): + return before == after + + def test_walrus_conversion_inline(): + a = "Hello" + assert not my_func(a, a := a.lower()) + assert a == "hello" + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_inline_walrus_operator_reverse(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def my_func(before, after): + return before == after + + def test_walrus_conversion_reverse(): + a = "Hello" + assert my_func(a := a.lower(), a) + assert a == 'hello' + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_no_variable_name_conflict( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_conversion_no_conflict(): + a = "Hello" + assert a == (b := a.lower()) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"]) + + def test_assertion_walrus_operator_true_assertion_and_changes_variable_value( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_conversion_succeed(): + a = "Hello" + assert a != (a := a.lower()) + assert a == 'hello' + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_fail_assertion(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_walrus_conversion_fails(): + a = "Hello" + assert a == (a := a.lower()) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"]) + + def test_assertion_walrus_operator_boolean_composite( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_operator_change_boolean_value(): + a = True + assert a and True and ((a := False) is False) and (a is False) and ((a := None) is None) + assert a is None + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_compare_boolean_fails( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_operator_change_boolean_value(): + a = True + assert not (a and ((a := False) is False)) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*assert not (True and False is False)"]) + + def test_assertion_walrus_operator_boolean_none_fails( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_operator_change_boolean_value(): + a = True + assert not (a and ((a := None) is None)) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*assert not (True and None is None)"]) + + def test_assertion_walrus_operator_value_changes_cleared_after_each_test( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def test_walrus_operator_change_value(): + a = True + assert (a := None) is None + + def test_walrus_operator_not_override_value(): + a = True + assert a is True + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + +class TestIssue11028: + def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_in_string(): + assert (obj := "foo") in obj + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_in_operand_json_dumps( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + import json + + def test_json_encoder(): + assert (obj := "foo") in json.dumps(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a): + return a + + def test_call_other_function_arg(): + assert (obj := "foo") == f(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function_keyword_arg( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a='test'): + return a + + def test_call_other_function_k_arg(): + assert (obj := "foo") == f(a=obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_equals_operand_function_arg_as_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def f(a='test'): + return a + + def test_function_of_function(): + assert (obj := "foo") == f(f(obj)) + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + def test_assertion_walrus_operator_gt_operand_function( + self, pytester: Pytester + ) -> None: + pytester.makepyfile( + """ + def add_one(a): + return a + 1 + + def test_gt(): + assert (obj := 4) > add_one(obj) + """ + ) + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"]) + + +class TestIssue11239: + def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None: + """Regression for (#11239) + + Walrus operator rewriting would leak to separate test cases if they used the same variables. + """ + pytester.makepyfile( + """ + def test_1(): + state = {"x": 2}.get("x") + assert state is not None + + def test_2(): + db = {"x": 2} + assert (state := db.get("x")) is not None + """ + ) + result = pytester.runpytest() + assert result.ret == 0 + + @pytest.mark.skipif( - sys.maxsize <= (2 ** 31 - 1), reason="Causes OverflowError on 32bit systems" + sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems" ) @pytest.mark.parametrize("offset", [-1, +1]) def test_source_mtime_long_long(pytester: Pytester, offset) -> None: @@ -1335,7 +1589,7 @@ def test_source_mtime_long_long(pytester: Pytester, offset) -> None: # use unsigned long timestamp which overflows signed long, # which was the cause of the bug # +1 offset also tests masking of 0xFFFFFFFF - timestamp = 2 ** 32 + offset + timestamp = 2**32 + offset os.utime(str(p), (timestamp, timestamp)) result = pytester.runpytest() assert result.ret == 0 @@ -1379,7 +1633,7 @@ class TestEarlyRewriteBailout: @pytest.fixture def hook( self, pytestconfig, monkeypatch, pytester: Pytester - ) -> AssertionRewritingHook: + ) -> Generator[AssertionRewritingHook, None, None]: """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track if PathFinder.find_spec has been called. """ @@ -1400,11 +1654,11 @@ class TestEarlyRewriteBailout: hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config - hook.fnpats[:] = ["test_*.py", "*_test.py"] - monkeypatch.setattr(hook, "_find_spec", spy_find_spec) - hook.set_session(StubSession()) # type: ignore[arg-type] - pytester.syspathinsert() - return hook + with mock.patch.object(hook, "fnpats", ["test_*.py", "*_test.py"]): + monkeypatch.setattr(hook, "_find_spec", spy_find_spec) + hook.set_session(StubSession()) # type: ignore[arg-type] + pytester.syspathinsert() + yield hook def test_basic(self, pytester: Pytester, hook: AssertionRewritingHook) -> None: """ @@ -1454,9 +1708,9 @@ class TestEarlyRewriteBailout: } ) pytester.syspathinsert("tests") - hook.fnpats[:] = ["tests/**.py"] - assert hook.find_spec("file") is not None - assert self.find_spec_calls == ["file"] + with mock.patch.object(hook, "fnpats", ["tests/**.py"]): + assert hook.find_spec("file") is not None + assert self.find_spec_calls == ["file"] @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" @@ -1477,8 +1731,8 @@ class TestEarlyRewriteBailout: import os import tempfile - with tempfile.TemporaryDirectory() as d: - os.chdir(d) + with tempfile.TemporaryDirectory() as newpath: + os.chdir(newpath) """, "test_test.py": """\ def test(): @@ -1602,10 +1856,10 @@ class TestAssertionPass: result.assert_outcomes(passed=1) +# fmt: off @pytest.mark.parametrize( ("src", "expected"), ( - # fmt: off pytest.param(b"", {}, id="trivial"), pytest.param( b"def x(): assert 1\n", @@ -1682,9 +1936,9 @@ class TestAssertionPass: {1: "5"}, id="no newline at end of file", ), - # fmt: on ), ) +# fmt: on def test_get_assertion_exprs(src, expected) -> None: assert _get_assertion_exprs(src) == expected @@ -1720,6 +1974,11 @@ def test_try_makedirs(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) assert not try_makedirs(p) + err = OSError() + err.errno = errno.ENOSYS + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) + assert not try_makedirs(p) + # unhandled OSError should raise err = OSError() err.errno = errno.ECHILD @@ -1741,17 +2000,11 @@ class TestPyCacheDir: ) def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - - if prefix is not None and sys.version_info < (3, 8): - pytest.skip("pycache_prefix not available in py<38") monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False) assert get_cache_dir(Path(source)) == Path(expected) @pytest.mark.skipif( - sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" - ) - @pytest.mark.skipif( sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), reason="#9298", ) @@ -1786,9 +2039,7 @@ class TestPyCacheDir: assert test_foo_pyc.is_file() # normal file: not touched by pytest, normal cache tag - bar_init_pyc = get_cache_dir(bar_init) / "__init__.{cache_tag}.pyc".format( - cache_tag=sys.implementation.cache_tag - ) + bar_init_pyc = get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc" assert bar_init_pyc.is_file() @@ -1809,13 +2060,15 @@ class TestReprSizeVerbosity: ) def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None: class FakeConfig: - def getoption(self, name: str) -> int: - assert name == "verbose" + def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: return verbose config = FakeConfig() assert _get_maxsize_for_saferepr(cast(Config, config)) == expected_size + def test_get_maxsize_for_saferepr_no_config(self) -> None: + assert _get_maxsize_for_saferepr(None) == DEFAULT_REPR_MAX_SIZE + def create_test_file(self, pytester: Pytester, size: int) -> None: pytester.makepyfile( f""" @@ -1839,3 +2092,17 @@ class TestReprSizeVerbosity: self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10) result = pytester.runpytest("-vv") result.stdout.no_fnmatch_line("*xxx...xxx*") + + +class TestIssue11140: + def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + 0 + + def test_foo(): + pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_cacheprovider.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_cacheprovider.py index cc6d547dfb1..ea662e87f07 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_cacheprovider.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_cacheprovider.py @@ -1,14 +1,21 @@ +from enum import auto +from enum import Enum import os -import shutil from pathlib import Path +import shutil +from typing import Any from typing import Generator from typing import List +from typing import Sequence +from typing import Tuple -import pytest +from _pytest.compat import assert_never from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester from _pytest.tmpdir import TempPathFactory +import pytest + pytest_plugins = ("pytester",) @@ -24,6 +31,21 @@ class TestNewAPI: p = config.cache.mkdir("name") assert p.is_dir() + def test_cache_dir_permissions(self, pytester: Pytester) -> None: + """The .pytest_cache directory should have world-readable permissions + (depending on umask). + + Regression test for #12308. + """ + pytester.makeini("[pytest]") + config = pytester.parseconfigure() + assert config.cache is not None + p = config.cache.mkdir("name") + assert p.is_dir() + # Instead of messing with umask, make sure .pytest_cache has the same + # permissions as the default that `mkdir` gives `p`. + assert (p.parent.stat().st_mode & 0o777) == (p.stat().st_mode & 0o777) + def test_config_cache_dataerror(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") config = pytester.parseconfigure() @@ -36,9 +58,11 @@ class TestNewAPI: assert val == -2 @pytest.mark.filterwarnings("ignore:could not create cache path") - def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None: + def test_cache_writefail_cachefile_silent(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") - pytester.path.joinpath(".pytest_cache").write_text("gone wrong") + pytester.path.joinpath(".pytest_cache").write_text( + "gone wrong", encoding="utf-8" + ) config = pytester.parseconfigure() cache = config.cache assert cache is not None @@ -87,7 +111,7 @@ class TestNewAPI: "*= warnings summary =*", "*/cacheprovider.py:*", " */cacheprovider.py:*: PytestCacheWarning: could not create cache path " - f"{unwritable_cache_dir}/v/cache/nodeids", + f"{unwritable_cache_dir}/v/cache/nodeids: *", ' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))', "*1 failed, 3 warnings in*", ] @@ -131,12 +155,10 @@ class TestNewAPI: def test_custom_rel_cache_dir(self, pytester: Pytester) -> None: rel_cache_dir = os.path.join("custom_cache_dir", "subdir") pytester.makeini( - """ + f""" [pytest] - cache_dir = {cache_dir} - """.format( - cache_dir=rel_cache_dir - ) + cache_dir = {rel_cache_dir} + """ ) pytester.makepyfile(test_errored="def test_error():\n assert False") pytester.runpytest() @@ -148,12 +170,10 @@ class TestNewAPI: tmp = tmp_path_factory.mktemp("tmp") abs_cache_dir = tmp / "custom_cache_dir" pytester.makeini( - """ + f""" [pytest] - cache_dir = {cache_dir} - """.format( - cache_dir=abs_cache_dir - ) + cache_dir = {abs_cache_dir} + """ ) pytester.makepyfile(test_errored="def test_error():\n assert False") pytester.runpytest() @@ -167,17 +187,17 @@ class TestNewAPI: """ [pytest] cache_dir = {cache_dir} - """.format( - cache_dir="$env_var" - ) + """.format(cache_dir="$env_var") ) pytester.makepyfile(test_errored="def test_error():\n assert False") pytester.runpytest() assert pytester.path.joinpath("custom_cache_dir").is_dir() -@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir"))) -def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: +@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "mydir/tox-env"))) +def test_cache_reportheader( + env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: pytester.makepyfile("""def test_foo(): pass""") if env: monkeypatch.setenv(*env) @@ -198,12 +218,10 @@ def test_cache_reportheader_external_abspath( pytester.makepyfile("def test_hello(): pass") pytester.makeini( - """ + f""" [pytest] - cache_dir = {abscache} - """.format( - abscache=external_cache - ) + cache_dir = {external_cache} + """ ) result = pytester.runpytest("-v") result.stdout.fnmatch_lines([f"cachedir: {external_cache}"]) @@ -420,7 +438,13 @@ class TestLastFailed: result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 failed in*"]) - def test_terminal_report_lastfailed(self, pytester: Pytester) -> None: + @pytest.mark.parametrize("parent", ("directory", "package")) + def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None: + if parent == "package": + pytester.makepyfile( + __init__="", + ) + test_a = pytester.makepyfile( test_a=""" def test_a1(): pass @@ -494,7 +518,6 @@ class TestLastFailed: def test_lastfailed_collectfailure( self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: - pytester.makepyfile( test_maybe=""" import os @@ -506,7 +529,7 @@ class TestLastFailed: """ ) - def rlf(fail_import, fail_run): + def rlf(fail_import: int, fail_run: int) -> Any: monkeypatch.setenv("FAILIMPORT", str(fail_import)) monkeypatch.setenv("FAILTEST", str(fail_run)) @@ -554,7 +577,9 @@ class TestLastFailed: """ ) - def rlf(fail_import, fail_run, args=()): + def rlf( + fail_import: int, fail_run: int, args: Sequence[str] = () + ) -> Tuple[Any, Any]: monkeypatch.setenv("FAILIMPORT", str(fail_import)) monkeypatch.setenv("FAILTEST", str(fail_run)) @@ -638,13 +663,11 @@ class TestLastFailed: assert result.ret == 1 pytester.makepyfile( - """ + f""" import pytest @pytest.{mark} def test(): assert 0 - """.format( - mark=mark - ) + """ ) result = pytester.runpytest() assert result.ret == 0 @@ -773,7 +796,7 @@ class TestLastFailed: result = pytester.runpytest("--lf", "--lfnf", "none") result.stdout.fnmatch_lines( [ - "collected 2 items / 2 deselected", + "collected 2 items / 2 deselected / 0 selected", "run-last-failure: no previously failed tests, deselecting all items.", "deselected=2", "* 2 deselected in *", @@ -849,6 +872,33 @@ class TestLastFailed: ] ) + def test_lastfailed_skip_collection_with_nesting(self, pytester: Pytester) -> None: + """Check that file skipping works even when the file with failures is + nested at a different level of the collection tree.""" + pytester.makepyfile( + **{ + "test_1.py": """ + def test_1(): pass + """, + "pkg/__init__.py": "", + "pkg/test_2.py": """ + def test_2(): assert False + """, + } + ) + # first run + result = pytester.runpytest() + result.stdout.fnmatch_lines(["collected 2 items", "*1 failed*1 passed*"]) + # second run - test_1.py is skipped. + result = pytester.runpytest("--lf") + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "run-last-failure: rerun previous 1 failure (skipped 1 file)", + "*= 1 failed in *", + ] + ) + def test_lastfailed_with_known_failures_not_being_selected( self, pytester: Pytester ) -> None: @@ -902,8 +952,10 @@ class TestLastFailed: "collected 1 item", "run-last-failure: rerun previous 1 failure (skipped 1 file)", "", - "<Module pkg1/test_1.py>", - " <Function test_renamed>", + "<Dir *>", + " <Dir pkg1>", + " <Module test_1.py>", + " <Function test_renamed>", ] ) @@ -932,8 +984,10 @@ class TestLastFailed: "*collected 1 item", "run-last-failure: 1 known failures not in selected tests", "", - "<Module pkg1/test_1.py>", - " <Function test_pass>", + "<Dir *>", + " <Dir pkg1>", + " <Module test_1.py>", + " <Function test_pass>", ], consecutive=True, ) @@ -947,8 +1001,10 @@ class TestLastFailed: "collected 2 items / 1 deselected / 1 selected", "run-last-failure: rerun previous 1 failure", "", - "<Module pkg1/test_1.py>", - " <Function test_fail>", + "<Dir *>", + " <Dir pkg1>", + " <Module test_1.py>", + " <Function test_fail>", "*= 1/2 tests collected (1 deselected) in *", ], ) @@ -977,10 +1033,12 @@ class TestLastFailed: "collected 3 items / 1 deselected / 2 selected", "run-last-failure: rerun previous 2 failures", "", - "<Module pkg1/test_1.py>", - " <Class TestFoo>", - " <Function test_fail>", - " <Function test_other>", + "<Dir *>", + " <Dir pkg1>", + " <Module test_1.py>", + " <Class TestFoo>", + " <Function test_fail>", + " <Function test_other>", "", "*= 2/3 tests collected (1 deselected) in *", ], @@ -1014,8 +1072,10 @@ class TestLastFailed: "collected 1 item", "run-last-failure: 1 known failures not in selected tests", "", - "<Module pkg1/test_1.py>", - " <Function test_pass>", + "<Dir *>", + " <Dir pkg1>", + " <Module test_1.py>", + " <Function test_pass>", "", "*= 1 test collected in*", ], @@ -1053,6 +1113,28 @@ class TestLastFailed: result = pytester.runpytest("--lf") result.assert_outcomes(failed=3) + def test_non_python_file_skipped( + self, + pytester: Pytester, + dummy_yaml_custom_test: None, + ) -> None: + pytester.makepyfile( + **{ + "test_bad.py": """def test_bad(): assert False""", + }, + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) + + result = pytester.runpytest("--lf") + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "run-last-failure: rerun previous 1 failure (skipped 1 file)", + "* 1 failed in *", + ] + ) + class TestNewFirst: def test_newfirst_usecase(self, pytester: Pytester) -> None: @@ -1080,7 +1162,9 @@ class TestNewFirst: ["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"] ) - p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n") + p1.write_text( + "def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8" + ) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) result = pytester.runpytest("--nf", "--collect-only", "-q") @@ -1153,7 +1237,8 @@ class TestNewFirst: p1.write_text( "import pytest\n" "@pytest.mark.parametrize('num', [1, 2, 3])\n" - "def test_1(num): assert num\n" + "def test_1(num): assert num\n", + encoding="utf-8", ) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) @@ -1193,20 +1278,41 @@ class TestReadme: assert self.check_readme(pytester) is True -def test_gitignore(pytester: Pytester) -> None: +class Action(Enum): + """Action to perform on the cache directory.""" + + MKDIR = auto() + SET = auto() + + +@pytest.mark.parametrize("action", list(Action)) +def test_gitignore( + pytester: Pytester, + action: Action, +) -> None: """Ensure we automatically create .gitignore file in the pytest_cache directory (#3286).""" from _pytest.cacheprovider import Cache config = pytester.parseconfig() cache = Cache.for_config(config, _ispytest=True) - cache.set("foo", "bar") + if action == Action.MKDIR: + cache.mkdir("foo") + elif action == Action.SET: + cache.set("foo", "bar") + else: + assert_never(action) msg = "# Created by pytest automatically.\n*\n" gitignore_path = cache._cachedir.joinpath(".gitignore") assert gitignore_path.read_text(encoding="UTF-8") == msg # Does not overwrite existing/custom one. - gitignore_path.write_text("custom") - cache.set("something", "else") + gitignore_path.write_text("custom", encoding="utf-8") + if action == Action.MKDIR: + cache.mkdir("something") + elif action == Action.SET: + cache.set("something", "else") + else: + assert_never(action) assert gitignore_path.read_text(encoding="UTF-8") == "custom" @@ -1249,3 +1355,8 @@ def test_cachedir_tag(pytester: Pytester) -> None: cache.set("foo", "bar") cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG") assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT + + +def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None: + result = pytester.runpytest("--cache-show", "--help") + assert result.ret == 0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_capture.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_capture.py index 1bc1f2f8db2..0521c3b6b04 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_capture.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_capture.py @@ -1,16 +1,16 @@ +# mypy: allow-untyped-defs import contextlib import io +from io import UnsupportedOperation import os import subprocess import sys import textwrap -from io import UnsupportedOperation from typing import BinaryIO from typing import cast from typing import Generator from typing import TextIO -import pytest from _pytest import capture from _pytest.capture import _get_multicapture from _pytest.capture import CaptureFixture @@ -20,6 +20,8 @@ from _pytest.capture import MultiCapture from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest + # note: py.io capture tests where copied from # pylib 1.4.20.dev2 (rev 13d9af95547e) @@ -502,13 +504,11 @@ class TestCaptureFixture: self, pytester: Pytester, method ) -> None: p = pytester.makepyfile( - """\ - def test_hello(cap{}): + f"""\ + def test_hello(cap{method}): print("xxx42xxx") assert 0 - """.format( - method - ) + """ ) result = pytester.runpytest(p) result.stdout.fnmatch_lines(["xxx42xxx"]) @@ -623,7 +623,7 @@ class TestCaptureFixture: self, pytester: Pytester, fixture: str, no_capture: bool ) -> None: pytester.makepyfile( - """\ + f"""\ def test_disabled({fixture}): print('captured before') with {fixture}.disabled(): @@ -633,9 +633,7 @@ class TestCaptureFixture: def test_normal(): print('test_normal executed') - """.format( - fixture=fixture - ) + """ ) args = ("-s",) if no_capture else () result = pytester.runpytest_subprocess(*args) @@ -680,7 +678,7 @@ class TestCaptureFixture: """Ensure that capsys and capfd can be used by other fixtures during setup and teardown.""" pytester.makepyfile( - """\ + f"""\ import sys import pytest @@ -702,9 +700,7 @@ class TestCaptureFixture: out, err = captured_print assert out == 'stdout contents begin\\n' assert err == 'stderr contents begin\\n' - """.format( - fixture=fixture - ) + """ ) result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -717,7 +713,7 @@ class TestCaptureFixture: ) -> None: """Ensure we can access setup and teardown buffers from teardown when using capsys/capfd (##3033)""" pytester.makepyfile( - """\ + f"""\ import sys import pytest import os @@ -734,9 +730,7 @@ class TestCaptureFixture: def test_a(fix): print("call out") sys.stderr.write("call err\\n") - """.format( - cap=cap - ) + """ ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -750,9 +744,10 @@ def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None: def pytest_runtest_setup(item): raise ValueError(42) """ - ) + ), + encoding="utf-8", ) - sub1.joinpath("test_mod.py").write_text("def test_func1(): pass") + sub1.joinpath("test_mod.py").write_text("def test_func1(): pass", encoding="utf-8") result = pytester.runpytest(pytester.path, "--traceconfig") result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"]) @@ -890,14 +885,26 @@ def test_dontreadfrominput() -> None: from _pytest.capture import DontReadFromInput f = DontReadFromInput() - assert f.buffer is f + assert f.buffer is f # type: ignore[comparison-overlap] assert not f.isatty() pytest.raises(OSError, f.read) pytest.raises(OSError, f.readlines) iter_f = iter(f) pytest.raises(OSError, next, iter_f) pytest.raises(UnsupportedOperation, f.fileno) + pytest.raises(UnsupportedOperation, f.flush) + assert not f.readable() + pytest.raises(UnsupportedOperation, f.seek, 0) + assert not f.seekable() + pytest.raises(UnsupportedOperation, f.tell) + pytest.raises(UnsupportedOperation, f.truncate, 0) + pytest.raises(UnsupportedOperation, f.write, b"") + pytest.raises(UnsupportedOperation, f.writelines, []) + assert not f.writable() + assert isinstance(f.encoding, str) f.close() # just for completeness + with f: + pass def test_captureresult() -> None: @@ -1035,15 +1042,12 @@ class TestFDCapture: pytest.raises(AssertionError, cap.suspend) assert repr(cap) == ( - "<FDCapture 1 oldfd={} _state='done' tmpfile={!r}>".format( - cap.targetfd_save, cap.tmpfile - ) + f"<FDCapture 1 oldfd={cap.targetfd_save} _state='done' tmpfile={cap.tmpfile!r}>" ) # Should not crash with missing "_old". + assert isinstance(cap.syscapture, capture.SysCapture) assert repr(cap.syscapture) == ( - "<SysCapture stdout _old=<UNSET> _state='done' tmpfile={!r}>".format( - cap.syscapture.tmpfile - ) + f"<SysCapture stdout _old=<UNSET> _state='done' tmpfile={cap.syscapture.tmpfile!r}>" ) def test_capfd_sys_stdout_mode(self, capfd) -> None: @@ -1184,7 +1188,6 @@ class TestTeeStdCapture(TestStdCapture): def test_capturing_error_recursive(self) -> None: r"""For TeeStdCapture since we passthrough stderr/stdout, cap1 should get all output, while cap2 should only get "cap2\n".""" - with self.getcapture() as cap1: print("cap1") with self.getcapture() as cap2: @@ -1340,6 +1343,7 @@ def test_capsys_results_accessible_by_attribute(capsys: CaptureFixture[str]) -> def test_fdcapture_tmpfile_remains_the_same() -> None: cap = StdCaptureFD(out=False, err=True) + assert isinstance(cap.err, capture.FDCapture) try: cap.start_capturing() capfile = cap.err.tmpfile @@ -1378,28 +1382,27 @@ def test_close_and_capture_again(pytester: Pytester) -> None: def test_capturing_and_logging_fundamentals(pytester: Pytester, method: str) -> None: # here we check a fundamental feature p = pytester.makepyfile( - """ + f""" import sys, os, logging from _pytest import capture cap = capture.MultiCapture( in_=None, out=None, - err=capture.%s, + err=capture.{method}, ) cap.start_capturing() logging.warning("hello1") outerr = cap.readouterr() - print("suspend, captured %%s" %%(outerr,)) + print("suspend, captured %s" %(outerr,)) logging.warning("hello2") cap.pop_outerr_to_orig() logging.warning("hello3") outerr = cap.readouterr() - print("suspend2, captured %%s" %% (outerr,)) + print("suspend2, captured %s" % (outerr,)) """ - % (method,) ) result = pytester.runpython(p) result.stdout.fnmatch_lines( @@ -1433,19 +1436,19 @@ def test_error_attribute_issue555(pytester: Pytester) -> None: not sys.platform.startswith("win"), reason="only on windows", ) -def test_py36_windowsconsoleio_workaround_non_standard_streams() -> None: +def test_windowsconsoleio_workaround_non_standard_streams() -> None: """ - Ensure _py36_windowsconsoleio_workaround function works with objects that + Ensure _windowsconsoleio_workaround function works with objects that do not implement the full ``io``-based stream protocol, for example execnet channels (#2666). """ - from _pytest.capture import _py36_windowsconsoleio_workaround + from _pytest.capture import _windowsconsoleio_workaround class DummyStream: def write(self, s): pass stream = cast(TextIO, DummyStream()) - _py36_windowsconsoleio_workaround(stream) + _windowsconsoleio_workaround(stream) def test_dontreadfrominput_has_encoding(pytester: Pytester) -> None: @@ -1509,9 +1512,9 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None: def pytest_runtest_logreport(report): if "test_global" in report.nodeid: if report.when == "teardown": - with open("caplog", "w") as f: + with open("caplog", "w", encoding="utf-8") as f: f.write(report.caplog) - with open("capstdout", "w") as f: + with open("capstdout", "w", encoding="utf-8") as f: f.write(report.capstdout) """ ) @@ -1541,14 +1544,14 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None: result = pytester.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 - with open("caplog") as f: + with open("caplog", encoding="utf-8") as f: caplog = f.read() assert "fix setup" in caplog assert "something in test" in caplog assert "fix teardown" in caplog - with open("capstdout") as f: + with open("capstdout", encoding="utf-8") as f: capstdout = f.read() assert "fix setup" in capstdout @@ -1565,16 +1568,16 @@ def test_capture_with_live_logging( # capture should work with live cli logging pytester.makepyfile( - """ + f""" import logging import sys logger = logging.getLogger(__name__) - def test_capture({0}): + def test_capture({capture_fixture}): print("hello") sys.stderr.write("world\\n") - captured = {0}.readouterr() + captured = {capture_fixture}.readouterr() assert captured.out == "hello\\n" assert captured.err == "world\\n" @@ -1582,11 +1585,9 @@ def test_capture_with_live_logging( print("next") logging.info("something") - captured = {0}.readouterr() + captured = {capture_fixture}.readouterr() assert captured.out == "next\\n" - """.format( - capture_fixture - ) + """ ) result = pytester.runpytest_subprocess("--log-cli-level=INFO") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_collection.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_collection.py index 6a8a5c1cef1..7f0790693a5 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_collection.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_collection.py @@ -1,12 +1,15 @@ +# mypy: allow-untyped-defs import os +from pathlib import Path import pprint import shutil import sys +import tempfile import textwrap -from pathlib import Path from typing import List +from typing import Type -import pytest +from _pytest.assertion.util import running_on_ci from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest from _pytest.main import _in_venv @@ -16,6 +19,7 @@ from _pytest.nodes import Item from _pytest.pathlib import symlink_or_skip from _pytest.pytester import HookRecorder from _pytest.pytester import Pytester +import pytest def ensure_file(file_path: Path) -> Path: @@ -99,7 +103,8 @@ class TestCollector: conftest=""" import pytest class CustomFile(pytest.File): - pass + def collect(self): + return [] def pytest_collect_file(file_path, parent): if file_path.suffix == ".xxx": return CustomFile.from_parent(path=file_path, parent=parent) @@ -140,7 +145,7 @@ class TestCollectFS: ensure_file(tmp_path / ".bzr" / "test_notfound.py") ensure_file(tmp_path / "normal" / "test_found.py") for x in tmp_path.rglob("test_*.py"): - x.write_text("def test_hello(): pass", "utf-8") + x.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-only") s = result.stdout.str() @@ -162,7 +167,7 @@ class TestCollectFS: bindir = "Scripts" if sys.platform.startswith("win") else "bin" ensure_file(pytester.path / "virtual" / bindir / fname) testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") - testfile.write_text("def test_hello(): pass") + testfile.write_text("def test_hello(): pass", encoding="utf-8") # by default, ignore tests inside a virtualenv result = pytester.runpytest() @@ -192,7 +197,7 @@ class TestCollectFS: # norecursedirs takes priority ensure_file(pytester.path / ".virtual" / bindir / fname) testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") - testfile.write_text("def test_hello(): pass") + testfile.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-in-virtualenv") result.stdout.no_fnmatch_line("*test_invenv*") # ...unless the virtualenv is explicitly given on the CLI @@ -231,10 +236,14 @@ class TestCollectFS: ) tmp_path = pytester.path ensure_file(tmp_path / "mydir" / "test_hello.py").write_text( - "def test_1(): pass" + "def test_1(): pass", encoding="utf-8" + ) + ensure_file(tmp_path / "xyz123" / "test_2.py").write_text( + "def test_2(): 0/0", encoding="utf-8" + ) + ensure_file(tmp_path / "xy" / "test_ok.py").write_text( + "def test_3(): pass", encoding="utf-8" ) - ensure_file(tmp_path / "xyz123" / "test_2.py").write_text("def test_2(): 0/0") - ensure_file(tmp_path / "xy" / "test_ok.py").write_text("def test_3(): pass") rec = pytester.inline_run() rec.assertoutcome(passed=1) rec = pytester.inline_run("xyz123/test_2.py") @@ -244,32 +253,55 @@ class TestCollectFS: pytester.makeini( """ [pytest] - testpaths = gui uts + testpaths = */tests """ ) tmp_path = pytester.path - ensure_file(tmp_path / "env" / "test_1.py").write_text("def test_env(): pass") - ensure_file(tmp_path / "gui" / "test_2.py").write_text("def test_gui(): pass") - ensure_file(tmp_path / "uts" / "test_3.py").write_text("def test_uts(): pass") + ensure_file(tmp_path / "a" / "test_1.py").write_text( + "def test_a(): pass", encoding="utf-8" + ) + ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text( + "def test_b(): pass", encoding="utf-8" + ) + ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text( + "def test_c(): pass", encoding="utf-8" + ) # executing from rootdir only tests from `testpaths` directories # are collected items, reprec = pytester.inline_genitems("-v") - assert [x.name for x in items] == ["test_gui", "test_uts"] + assert [x.name for x in items] == ["test_b", "test_c"] # check that explicitly passing directories in the command-line # collects the tests - for dirname in ("env", "gui", "uts"): + for dirname in ("a", "b", "c"): items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname)) assert [x.name for x in items] == ["test_%s" % dirname] # changing cwd to each subdirectory and running pytest without # arguments collects the tests in that directory normally - for dirname in ("env", "gui", "uts"): + for dirname in ("a", "b", "c"): monkeypatch.chdir(pytester.path.joinpath(dirname)) items, reprec = pytester.inline_genitems() assert [x.name for x in items] == ["test_%s" % dirname] + def test_missing_permissions_on_unselected_directory_doesnt_crash( + self, pytester: Pytester + ) -> None: + """Regression test for #12120.""" + test = pytester.makepyfile(test="def test(): pass") + bad = pytester.mkdir("bad") + try: + bad.chmod(0) + + result = pytester.runpytest(test) + finally: + bad.chmod(750) + bad.rmdir() + + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=1) + class TestCollectPluginHookRelay: def test_pytest_collect_file(self, pytester: Pytester) -> None: @@ -324,17 +356,39 @@ class TestPrunetraceback: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_make_collect_report(): - outcome = yield - rep = outcome.get_result() + rep = yield rep.headerlines += ["header1"] - outcome.force_result(rep) + return rep """ ) result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"]) + def test_collection_error_traceback_is_clean(self, pytester: Pytester) -> None: + """When a collection error occurs, the report traceback doesn't contain + internal pytest stack entries. + + Issue #11710. + """ + pytester.makepyfile( + """ + raise Exception("LOUSY") + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*ERROR collecting*", + "test_*.py:1: in <module>", + ' raise Exception("LOUSY")', + "E Exception: LOUSY", + "*= short test summary info =*", + ], + consecutive=True, + ) + class TestCustomConftests: def test_ignore_collect_path(self, pytester: Pytester) -> None: @@ -345,8 +399,8 @@ class TestCustomConftests: """ ) sub = pytester.mkdir("xy123") - ensure_file(sub / "test_hello.py").write_text("syntax error") - sub.joinpath("conftest.py").write_text("syntax error") + ensure_file(sub / "test_hello.py").write_text("syntax error", encoding="utf-8") + sub.joinpath("conftest.py").write_text("syntax error", encoding="utf-8") pytester.makepyfile("def test_hello(): pass") pytester.makepyfile(test_one="syntax error") result = pytester.runpytest("--fulltrace") @@ -480,7 +534,7 @@ class TestSession: # assert root2 == rcol, rootid colitems = rcol.perform_collect([rcol.nodeid], genitems=False) assert len(colitems) == 1 - assert colitems[0].path == p + assert colitems[0].path == topdir def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: """Return pytest.Item instances reported by the pytest_collectreport hook""" @@ -501,7 +555,7 @@ class TestSession: newid = item.nodeid assert newid == id pprint.pprint(hookrec.calls) - topdir = pytester.path # noqa + topdir = pytester.path # noqa: F841 hookrec.assert_contains( [ ("pytest_collectstart", "collector.path == topdir"), @@ -637,6 +691,23 @@ class TestSession: # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] + def test_collect_parametrized_order(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize('i', [0, 1, 2]) + def test_param(i): ... + """ + ) + items, hookrec = pytester.inline_genitems(f"{p}::test_param") + assert len(items) == 3 + assert [item.nodeid for item in items] == [ + "test_collect_parametrized_order.py::test_param[0]", + "test_collect_parametrized_order.py::test_param[1]", + "test_collect_parametrized_order.py::test_param[2]", + ] + class Test_getinitialnodes: def test_global_file(self, pytester: Pytester) -> None: @@ -647,11 +718,12 @@ class Test_getinitialnodes: assert isinstance(col, pytest.Module) assert col.name == "x.py" assert col.parent is not None - assert col.parent.parent is None + assert col.parent.parent is not None + assert col.parent.parent.parent is None for parent in col.listchain(): assert parent.config is config - def test_pkgfile(self, pytester: Pytester) -> None: + def test_pkgfile(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: """Verify nesting when a module is within a package. The parent chain should match: Module<x.py> -> Package<subdir> -> Session. Session's parent should always be None. @@ -660,7 +732,8 @@ class Test_getinitialnodes: subdir = tmp_path.joinpath("subdir") x = ensure_file(subdir / "x.py") ensure_file(subdir / "__init__.py") - with subdir.cwd(): + with monkeypatch.context() as mp: + mp.chdir(subdir) config = pytester.parseconfigure(x) col = pytester.getnode(config, x) assert col is not None @@ -730,6 +803,20 @@ class Test_genitems: assert s.endswith("test_example_items1.testone") print(s) + def test_classmethod_is_discovered(self, pytester: Pytester) -> None: + """Test that classmethods are discovered""" + p = pytester.makepyfile( + """ + class TestCase: + @classmethod + def test_classmethod(cls) -> None: + pass + """ + ) + items, reprec = pytester.inline_genitems(p) + ids = [x.getmodpath() for x in items] # type: ignore[attr-defined] + assert ids == ["TestCase.test_classmethod"] + def test_class_and_functions_discovery_using_glob(self, pytester: Pytester) -> None: """Test that Python_classes and Python_functions config options work as prefixes and glob-like patterns (#600).""" @@ -881,6 +968,76 @@ class TestNodeKeywords: assert item.keywords["kw"] == "method" assert len(item.keywords) == len(set(item.keywords)) + def test_unpacked_marks_added_to_keywords(self, pytester: Pytester) -> None: + item = pytester.getitem( + """ + import pytest + pytestmark = pytest.mark.foo + class TestClass: + pytestmark = pytest.mark.bar + def test_method(self): pass + test_method.pytestmark = pytest.mark.baz + """, + "test_method", + ) + assert isinstance(item, pytest.Function) + cls = item.getparent(pytest.Class) + assert cls is not None + mod = item.getparent(pytest.Module) + assert mod is not None + + assert item.keywords["foo"] == pytest.mark.foo.mark + assert item.keywords["bar"] == pytest.mark.bar.mark + assert item.keywords["baz"] == pytest.mark.baz.mark + + assert cls.keywords["foo"] == pytest.mark.foo.mark + assert cls.keywords["bar"] == pytest.mark.bar.mark + assert "baz" not in cls.keywords + + assert mod.keywords["foo"] == pytest.mark.foo.mark + assert "bar" not in mod.keywords + assert "baz" not in mod.keywords + + +class TestCollectDirectoryHook: + def test_custom_directory_example(self, pytester: Pytester) -> None: + """Verify the example from the customdirectory.rst doc.""" + pytester.copy_example("customdirectory") + + reprec = pytester.inline_run() + + reprec.assertoutcome(passed=2, failed=0) + calls = reprec.getcalls("pytest_collect_directory") + assert len(calls) == 2 + assert calls[0].path == pytester.path + assert isinstance(calls[0].parent, pytest.Session) + assert calls[1].path == pytester.path / "tests" + assert isinstance(calls[1].parent, pytest.Dir) + + def test_directory_ignored_if_none(self, pytester: Pytester) -> None: + """If the (entire) hook returns None, it's OK, the directory is ignored.""" + pytester.makeconftest( + """ + import pytest + + @pytest.hookimpl(wrapper=True) + def pytest_collect_directory(): + yield + return None + """, + ) + pytester.makepyfile( + **{ + "tests/test_it.py": """ + import pytest + + def test_it(): pass + """, + }, + ) + reprec = pytester.inline_run() + reprec.assertoutcome(passed=0, failed=0) + COLLECTION_ERROR_PY_FILES = dict( test_01_failure=""" @@ -1011,13 +1168,18 @@ def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None: def fix(): return 1 """ - ) + ), + encoding="utf-8", + ) + foo_path.joinpath("test_foo.py").write_text( + "def test_foo(fix): assert fix == 1", encoding="utf-8" ) - foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1") # Tests in `food/` should not see the conftest fixture from `foo/` food_path = pytester.mkpydir("food") - food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1") + food_path.joinpath("test_food.py").write_text( + "def test_food(fix): assert fix == 1", encoding="utf-8" + ) res = pytester.runpytest() assert res.ret == 1 @@ -1038,22 +1200,24 @@ def test_collect_init_tests(pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "collected 2 items", - "<Package tests>", - " <Module __init__.py>", - " <Function test_init>", - " <Module test_foo.py>", - " <Function test_foo>", + "<Dir *>", + " <Package tests>", + " <Module __init__.py>", + " <Function test_init>", + " <Module test_foo.py>", + " <Function test_foo>", ] ) result = pytester.runpytest("./tests", "--collect-only") result.stdout.fnmatch_lines( [ "collected 2 items", - "<Package tests>", - " <Module __init__.py>", - " <Function test_init>", - " <Module test_foo.py>", - " <Function test_foo>", + "<Dir *>", + " <Package tests>", + " <Module __init__.py>", + " <Function test_init>", + " <Module test_foo.py>", + " <Function test_foo>", ] ) # Ignores duplicates with "." and pkginit (#4310). @@ -1061,11 +1225,12 @@ def test_collect_init_tests(pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "collected 2 items", - "<Package tests>", - " <Module __init__.py>", - " <Function test_init>", - " <Module test_foo.py>", - " <Function test_foo>", + "<Dir *>", + " <Package tests>", + " <Module __init__.py>", + " <Function test_init>", + " <Module test_foo.py>", + " <Function test_foo>", ] ) # Same as before, but different order. @@ -1073,21 +1238,32 @@ def test_collect_init_tests(pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "collected 2 items", - "<Package tests>", - " <Module __init__.py>", - " <Function test_init>", - " <Module test_foo.py>", - " <Function test_foo>", + "<Dir *>", + " <Package tests>", + " <Module __init__.py>", + " <Function test_init>", + " <Module test_foo.py>", + " <Function test_foo>", ] ) result = pytester.runpytest("./tests/test_foo.py", "--collect-only") result.stdout.fnmatch_lines( - ["<Package tests>", " <Module test_foo.py>", " <Function test_foo>"] + [ + "<Dir *>", + " <Package tests>", + " <Module test_foo.py>", + " <Function test_foo>", + ] ) result.stdout.no_fnmatch_line("*test_init*") result = pytester.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( - ["<Package tests>", " <Module __init__.py>", " <Function test_init>"] + [ + "<Dir *>", + " <Package tests>", + " <Module __init__.py>", + " <Function test_init>", + ] ) result.stdout.no_fnmatch_line("*test_foo*") @@ -1143,23 +1319,21 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") pytester.path.joinpath("conftest.py").write_text( textwrap.dedent( - """ + f""" import os - os.chdir(%r) + os.chdir({str(subdir)!r}) """ - % (str(subdir),) - ) + ), + encoding="utf-8", ) pytester.makepyfile( - """ + f""" def test_1(): import os - assert os.getcwd() == %r + assert os.getcwd() == {str(subdir)!r} """ - % (str(subdir),) ) - with pytester.path.cwd(): - result = pytester.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed in*"]) assert result.ret == 0 @@ -1170,8 +1344,7 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None: testpaths = . """ ) - with pytester.path.cwd(): - result = pytester.runpytest("--collect-only") + result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines(["collected 1 item"]) @@ -1180,8 +1353,12 @@ def test_collect_pyargs_with_testpaths( ) -> None: testmod = pytester.mkdir("testmod") # NOTE: __init__.py is not collected since it does not match python_files. - testmod.joinpath("__init__.py").write_text("def test_func(): pass") - testmod.joinpath("test_file.py").write_text("def test_func(): pass") + testmod.joinpath("__init__.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) + testmod.joinpath("test_file.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) root = pytester.mkdir("root") root.joinpath("pytest.ini").write_text( @@ -1191,14 +1368,66 @@ def test_collect_pyargs_with_testpaths( addopts = --pyargs testpaths = testmod """ - ) + ), + encoding="utf-8", ) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) - with root.cwd(): + with monkeypatch.context() as mp: + mp.chdir(root) result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed in*"]) +def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: + """The testpaths ini option should load conftests in those paths as 'initial' (#10987).""" + p = pytester.mkdir("some_path") + p.joinpath("conftest.py").write_text( + textwrap.dedent( + """ + def pytest_sessionstart(session): + raise Exception("pytest_sessionstart hook successfully run") + """ + ), + encoding="utf-8", + ) + pytester.makeini( + """ + [pytest] + testpaths = some_path + """ + ) + + # No command line args - falls back to testpaths. + result = pytester.runpytest() + assert result.ret == ExitCode.INTERNAL_ERROR + result.stdout.fnmatch_lines( + "INTERNALERROR* Exception: pytest_sessionstart hook successfully run" + ) + + # No fallback. + result = pytester.runpytest(".") + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + +def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None: + """Long option values do not break initial conftests handling (#10169).""" + option_value = "x" * 1024 * 1000 + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addoption("--xx", default=None) + """ + ) + pytester.makepyfile( + f""" + def test_foo(request): + assert request.config.getoption("xx") == {option_value!r} + """ + ) + result = pytester.runpytest(f"--xx={option_value}") + assert result.ret == 0 + + def test_collect_symlink_file_arg(pytester: Pytester) -> None: """Collect a direct symlink works even if it does not match python_files (#4325).""" real = pytester.makepyfile( @@ -1226,6 +1455,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None: assert request.node.nodeid == "test_real.py::test_nodeid" """ ), + encoding="utf-8", ) out_of_tree = pytester.mkdir("out_of_tree") @@ -1254,12 +1484,16 @@ def test_collect_symlink_dir(pytester: Pytester) -> None: def test_collectignore_via_conftest(pytester: Pytester) -> None: """collect_ignore in parent conftest skips importing child (issue #4592).""" tests = pytester.mkpydir("tests") - tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']") + tests.joinpath("conftest.py").write_text( + "collect_ignore = ['ignore_me']", encoding="utf-8" + ) ignore_me = tests.joinpath("ignore_me") ignore_me.mkdir() ignore_me.joinpath("__init__.py").touch() - ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'") + ignore_me.joinpath("conftest.py").write_text( + "assert 0, 'should_not_be_called'", encoding="utf-8" + ) result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -1268,23 +1502,31 @@ def test_collectignore_via_conftest(pytester: Pytester) -> None: def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") - init.write_text("def test_init(): pass") + init.write_text("def test_init(): pass", encoding="utf-8") p = subdir.joinpath("test_file.py") - p.write_text("def test_file(): pass") + p.write_text("def test_file(): pass", encoding="utf-8") - # NOTE: without "-o python_files=*.py" this collects test_file.py twice. - # This changed/broke with "Add package scoped fixtures #2283" (2b1410895) - # initially (causing a RecursionError). - result = pytester.runpytest("-v", str(init), str(p)) + # Just the package directory, the __init__.py module is filtered out. + result = pytester.runpytest("-v", subdir) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", + "*1 passed in*", + ] + ) + + # But it's included if specified directly. + result = pytester.runpytest("-v", init, p) + result.stdout.fnmatch_lines( + [ + "sub/__init__.py::test_init PASSED*", "sub/test_file.py::test_file PASSED*", "*2 passed in*", ] ) - result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) + # Or if the pattern allows it. + result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir) result.stdout.fnmatch_lines( [ "sub/__init__.py::test_init PASSED*", @@ -1297,12 +1539,15 @@ def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: def test_collect_pkg_init_only(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") - init.write_text("def test_init(): pass") + init.write_text("def test_init(): pass", encoding="utf-8") - result = pytester.runpytest(str(init)) + result = pytester.runpytest(subdir) result.stdout.fnmatch_lines(["*no tests ran in*"]) - result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) + result = pytester.runpytest("-v", init) + result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) + + result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) @@ -1312,7 +1557,7 @@ def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None: sub = pytester.mkdir("sub") if use_pkg: sub.joinpath("__init__.py").touch() - sub.joinpath("test_file.py").write_text("def test_file(): pass") + sub.joinpath("test_file.py").write_text("def test_file(): pass", encoding="utf-8") # Create a broken symlink. symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py")) @@ -1350,7 +1595,7 @@ def test_collector_respects_tbstyle(pytester: Pytester) -> None: def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None: pytester.makepyfile("def test(): pass") pydir = pytester.mkpydir("foopkg") - pydir.joinpath("__init__.py").write_text("assert False") + pydir.joinpath("__init__.py").write_text("assert False", encoding="utf-8") result = pytester.runpytest() assert result.ret == ExitCode.OK @@ -1379,13 +1624,16 @@ def test_fscollector_from_parent(pytester: Pytester, request: FixtureRequest) -> super().__init__(*k, **kw) self.x = x + def collect(self): + raise NotImplementedError() + collector = MyCollector.from_parent( parent=request.session, path=pytester.path / "foo", x=10 ) assert collector.x == 10 -def test_class_from_parent(pytester: Pytester, request: FixtureRequest) -> None: +def test_class_from_parent(request: FixtureRequest) -> None: """Ensure Class.from_parent can forward custom arguments to the constructor.""" class MyCollector(pytest.Class): @@ -1426,13 +1674,11 @@ class TestImportModeImportlib: pytester.makepyfile( **{ "tests/conftest.py": "", - "tests/test_foo.py": """ + "tests/test_foo.py": f""" import sys def test_check(): assert r"{tests_dir}" not in sys.path - """.format( - tests_dir=tests_dir - ), + """, } ) result = pytester.runpytest("-v", "--import-mode=importlib") @@ -1477,6 +1723,35 @@ class TestImportModeImportlib: ] ) + def test_using_python_path(self, pytester: Pytester) -> None: + """ + Dummy modules created by insert_missing_modules should not get in + the way of modules that could be imported via python path (#9645). + """ + pytester.makeini( + """ + [pytest] + pythonpath = . + addopts = --import-mode importlib + """ + ) + pytester.makepyfile( + **{ + "tests/__init__.py": "", + "tests/conftest.py": "", + "tests/subpath/__init__.py": "", + "tests/subpath/helper.py": "", + "tests/subpath/test_something.py": """ + import tests.subpath.helper + + def test_something(): + assert True + """, + } + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines("*1 passed in*") + def test_does_not_crash_on_error_from_decorated_function(pytester: Pytester) -> None: """Regression test for an issue around bad exception formatting due to @@ -1504,3 +1779,129 @@ def test_does_not_crash_on_recursive_symlink(pytester: Pytester) -> None: assert result.ret == ExitCode.OK assert result.parseoutcomes() == {"passed": 1} + + +@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only") +def test_collect_short_file_windows(pytester: Pytester) -> None: + """Reproducer for #11895: short paths not collected on Windows.""" + short_path = tempfile.mkdtemp() + if "~" not in short_path: # pragma: no cover + if running_on_ci(): + # On CI, we are expecting that under the current GitHub actions configuration, + # tempfile.mkdtemp() is producing short paths, so we want to fail to prevent + # this from silently changing without us noticing. + pytest.fail( + f"tempfile.mkdtemp() failed to produce a short path on CI: {short_path}" + ) + else: + # We want to skip failing this test locally in this situation because + # depending on the local configuration tempfile.mkdtemp() might not produce a short path: + # For example, user might have configured %TEMP% exactly to avoid generating short paths. + pytest.skip( + f"tempfile.mkdtemp() failed to produce a short path: {short_path}, skipping" + ) + + test_file = Path(short_path).joinpath("test_collect_short_file_windows.py") + test_file.write_text("def test(): pass", encoding="UTF-8") + result = pytester.runpytest(short_path) + assert result.parseoutcomes() == {"passed": 1} + + +def test_pyargs_collection_tree(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + """When using `--pyargs`, the collection tree of a pyargs collection + argument should only include parents in the import path, not up to confcutdir. + + Regression test for #11904. + """ + site_packages = pytester.path / "venv/lib/site-packages" + site_packages.mkdir(parents=True) + monkeypatch.syspath_prepend(site_packages) + pytester.makepyfile( + **{ + "venv/lib/site-packages/pkg/__init__.py": "", + "venv/lib/site-packages/pkg/sub/__init__.py": "", + "venv/lib/site-packages/pkg/sub/test_it.py": "def test(): pass", + } + ) + + result = pytester.runpytest("--pyargs", "--collect-only", "pkg.sub.test_it") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines( + [ + "<Package venv/lib/site-packages/pkg>", + " <Package sub>", + " <Module test_it.py>", + " <Function test>", + ], + consecutive=True, + ) + + # Now with an unrelated rootdir with unrelated files. + monkeypatch.chdir(tempfile.gettempdir()) + + result = pytester.runpytest("--pyargs", "--collect-only", "pkg.sub.test_it") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines( + [ + "<Package *pkg>", + " <Package sub>", + " <Module test_it.py>", + " <Function test>", + ], + consecutive=True, + ) + + +def test_do_not_collect_symlink_siblings( + pytester: Pytester, tmp_path: Path, request: pytest.FixtureRequest +) -> None: + """ + Regression test for #12039: Do not collect from directories that are symlinks to other directories in the same path. + + The check for short paths under Windows via os.path.samefile, introduced in #11936, also finds the symlinked + directory created by tmp_path/tmpdir. + """ + # Use tmp_path because it creates a symlink with the name "current" next to the directory it creates. + symlink_path = tmp_path.parent / (tmp_path.name[:-1] + "current") + assert symlink_path.is_symlink() is True + + # Create test file. + tmp_path.joinpath("test_foo.py").write_text("def test(): pass", encoding="UTF-8") + + # Ensure we collect it only once if we pass the tmp_path. + result = pytester.runpytest(tmp_path, "-sv") + result.assert_outcomes(passed=1) + + # Ensure we collect it only once if we pass the symlinked directory. + result = pytester.runpytest(symlink_path, "-sv") + result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize( + "exception_class, msg", + [ + (KeyboardInterrupt, "*!!! KeyboardInterrupt !!!*"), + (SystemExit, "INTERNALERROR> SystemExit"), + ], +) +def test_respect_system_exceptions( + pytester: Pytester, + exception_class: Type[BaseException], + msg: str, +): + head = "Before exception" + tail = "After exception" + ensure_file(pytester.path / "test_eggs.py").write_text( + f"print('{head}')", encoding="UTF-8" + ) + ensure_file(pytester.path / "test_ham.py").write_text( + f"raise {exception_class.__name__}()", encoding="UTF-8" + ) + ensure_file(pytester.path / "test_spam.py").write_text( + f"print('{tail}')", encoding="UTF-8" + ) + + result = pytester.runpytest_subprocess("-s") + result.stdout.fnmatch_lines([f"*{head}*"]) + result.stdout.fnmatch_lines([msg]) + result.stdout.no_fnmatch_line(f"*{tail}*") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_compat.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_compat.py index 37cf4a077d7..73ac1bad858 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_compat.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_compat.py @@ -1,20 +1,22 @@ +# mypy: allow-untyped-defs import enum -import sys +from functools import cached_property from functools import partial from functools import wraps +import sys from typing import TYPE_CHECKING from typing import Union -import pytest from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.outcomes import OutcomeException from _pytest.pytester import Pytester +import pytest + if TYPE_CHECKING: from typing_extensions import Literal @@ -92,7 +94,7 @@ def test_get_real_func_partial() -> None: assert get_real_func(partial(foo)) is foo -@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed") +@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed") def test_is_generator_asyncio(pytester: Pytester) -> None: pytester.makepyfile( """ @@ -135,7 +137,7 @@ def test_is_generator_async_gen_syntax(pytester: Pytester) -> None: pytester.makepyfile( """ from _pytest.compat import is_generator - def test_is_generator_py36(): + def test_is_generator(): async def foo(): yield await foo() @@ -167,17 +169,17 @@ class ErrorsHelper: def test_helper_failures() -> None: helper = ErrorsHelper() - with pytest.raises(Exception): - helper.raise_exception + with pytest.raises(Exception): # noqa: B017 + _ = helper.raise_exception with pytest.raises(OutcomeException): - helper.raise_fail_outcome + _ = helper.raise_fail_outcome def test_safe_getattr() -> None: helper = ErrorsHelper() assert safe_getattr(helper, "raise_exception", "default") == "default" assert safe_getattr(helper, "raise_fail_outcome", "default") == "default" - with pytest.raises(BaseException): + with pytest.raises(BaseException): # noqa: B017 assert safe_getattr(helper, "raise_baseexception", "default") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_config.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_config.py index 8013966f071..0d097f71622 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_config.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_config.py @@ -1,8 +1,12 @@ +# mypy: allow-untyped-defs +import dataclasses +import importlib.metadata import os +from pathlib import Path import re import sys import textwrap -from pathlib import Path +from typing import Any from typing import Dict from typing import List from typing import Sequence @@ -10,11 +14,7 @@ from typing import Tuple from typing import Type from typing import Union -import attr - import _pytest._code -import pytest -from _pytest.compat import importlib_metadata from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool @@ -22,6 +22,8 @@ from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import ExitCode from _pytest.config import parse_warning_filter +from _pytest.config.argparsing import get_ini_default_for_type +from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor @@ -29,6 +31,7 @@ from _pytest.config.findpaths import locate_config from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import absolutepath from _pytest.pytester import Pytester +import pytest class TestParseIni: @@ -48,16 +51,14 @@ class TestParseIni: monkeypatch.chdir(sub) (tmp_path / filename).write_text( textwrap.dedent( - """\ + f"""\ [{section}] name = value - """.format( - section=section - ) + """ ), encoding="utf-8", ) - _, _, cfg = locate_config([sub]) + _, _, cfg = locate_config(Path.cwd(), [sub]) assert cfg["name"] == "value" config = pytester.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" @@ -75,7 +76,7 @@ class TestParseIni: % p1.name, ) result = pytester.runpytest() - result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"]) + result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"]) assert result.ret == 0 def test_append_parse_args( @@ -88,7 +89,8 @@ class TestParseIni: [pytest] addopts = --verbose """ - ) + ), + encoding="utf-8", ) config = pytester.parseconfig(tmp_path) assert config.option.color == "no" @@ -112,32 +114,66 @@ class TestParseIni: @pytest.mark.parametrize( "section, name", - [("tool:pytest", "setup.cfg"), ("pytest", "tox.ini"), ("pytest", "pytest.ini")], + [ + ("tool:pytest", "setup.cfg"), + ("pytest", "tox.ini"), + ("pytest", "pytest.ini"), + ("pytest", ".pytest.ini"), + ], ) def test_ini_names(self, pytester: Pytester, name, section) -> None: pytester.path.joinpath(name).write_text( textwrap.dedent( - """ + f""" [{section}] - minversion = 1.0 - """.format( - section=section - ) - ) + minversion = 3.36 + """ + ), + encoding="utf-8", ) config = pytester.parseconfig() - assert config.getini("minversion") == "1.0" + assert config.getini("minversion") == "3.36" def test_pyproject_toml(self, pytester: Pytester) -> None: - pytester.makepyprojecttoml( + pyproject_toml = pytester.makepyprojecttoml( """ [tool.pytest.ini_options] minversion = "1.0" """ ) config = pytester.parseconfig() + assert config.inipath == pyproject_toml assert config.getini("minversion") == "1.0" + def test_empty_pyproject_toml(self, pytester: Pytester) -> None: + """An empty pyproject.toml is considered as config if no other option is found.""" + pyproject_toml = pytester.makepyprojecttoml("") + config = pytester.parseconfig() + assert config.inipath == pyproject_toml + + def test_empty_pyproject_toml_found_many(self, pytester: Pytester) -> None: + """ + In case we find multiple pyproject.toml files in our search, without a [tool.pytest.ini_options] + table and without finding other candidates, the closest to where we started wins. + """ + pytester.makefile( + ".toml", + **{ + "pyproject": "", + "foo/pyproject": "", + "foo/bar/pyproject": "", + }, + ) + config = pytester.parseconfig(pytester.path / "foo/bar") + assert config.inipath == pytester.path / "foo/bar/pyproject.toml" + + def test_pytest_ini_trumps_pyproject_toml(self, pytester: Pytester) -> None: + """A pytest.ini always take precedence over a pyproject.toml file.""" + pytester.makepyprojecttoml("[tool.pytest.ini_options]") + pytest_ini = pytester.makefile(".ini", pytest="") + config = pytester.parseconfig() + assert config.inipath == pytest_ini + def test_toxini_before_lower_pytestini(self, pytester: Pytester) -> None: sub = pytester.mkdir("sub") sub.joinpath("tox.ini").write_text( @@ -146,7 +182,8 @@ class TestParseIni: [pytest] minversion = 2.0 """ - ) + ), + encoding="utf-8", ) pytester.path.joinpath("pytest.ini").write_text( textwrap.dedent( @@ -154,16 +191,46 @@ class TestParseIni: [pytest] minversion = 1.5 """ - ) + ), + encoding="utf-8", ) config = pytester.parseconfigure(sub) assert config.getini("minversion") == "2.0" def test_ini_parse_error(self, pytester: Pytester) -> None: - pytester.path.joinpath("pytest.ini").write_text("addopts = -x") + pytester.path.joinpath("pytest.ini").write_text( + "addopts = -x", encoding="utf-8" + ) result = pytester.runpytest() assert result.ret != 0 - result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"]) + result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined") + + def test_toml_parse_error(self, pytester: Pytester) -> None: + pytester.makepyprojecttoml( + """ + \\" + """ + ) + result = pytester.runpytest() + assert result.ret != 0 + result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*") + + def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None: + # If --confcutdir is not specified, and there is no configfile, default + # to the rootpath. + sub = pytester.mkdir("sub") + os.chdir(sub) + config = pytester.parseconfigure() + assert config.pluginmanager._confcutdir == sub + + def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None: + # If --confcutdir is not specified, and there is a configfile, default + # to the configfile's directory. + pytester.makeini("[pytest]") + sub = pytester.mkdir("sub") + os.chdir(sub) + config = pytester.parseconfigure() + assert config.pluginmanager._confcutdir == pytester.path @pytest.mark.xfail(reason="probably not needed") def test_confcutdir(self, pytester: Pytester) -> None: @@ -408,11 +475,11 @@ class TestParseIni: This test installs a mock "myplugin-1.5" which is used in the parametrized test cases. """ - @attr.s + @dataclasses.dataclass class DummyEntryPoint: - name = attr.ib() - module = attr.ib() - group = "pytest11" + name: str + module: str + group: str = "pytest11" def load(self): __import__(self.module) @@ -422,11 +489,11 @@ class TestParseIni: DummyEntryPoint("myplugin1", "myplugin1_module"), ] - @attr.s + @dataclasses.dataclass class DummyDist: - entry_points = attr.ib() - files = () - version = plugin_version + entry_points: object + files: object = () + version: str = plugin_version @property def metadata(self): @@ -438,7 +505,7 @@ class TestParseIni: pytester.makepyfile(myplugin1_module="# my plugin module") pytester.syspathinsert() - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) pytester.makeini(ini_file_text) @@ -470,6 +537,24 @@ class TestParseIni: result = pytester.runpytest("--foo=1") result.stdout.fnmatch_lines("* no tests ran in *") + def test_args_source_args(self, pytester: Pytester): + config = pytester.parseconfig("--", "test_filename.py") + assert config.args_source == Config.ArgsSource.ARGS + + def test_args_source_invocation_dir(self, pytester: Pytester): + config = pytester.parseconfig() + assert config.args_source == Config.ArgsSource.INVOCATION_DIR + + def test_args_source_testpaths(self, pytester: Pytester): + pytester.makeini( + """ + [pytest] + testpaths=* + """ + ) + config = pytester.parseconfig() + assert config.args_source == Config.ArgsSource.TESTPATHS + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, pytester: Pytester) -> None: @@ -500,6 +585,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom.ini") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom.ini") + assert config.getini("custom") == "1" pytester.makefile( ".cfg", @@ -510,6 +597,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg") + assert config.getini("custom") == "1" pytester.makefile( ".toml", @@ -522,6 +611,8 @@ class TestConfigCmdlineParsing: ) config = pytester.parseconfig("-c", "custom.toml") assert config.getini("custom") == "1" + config = pytester.parseconfig("--config-file", "custom.toml") + assert config.getini("custom") == "1" def test_absolute_win32_path(self, pytester: Pytester) -> None: temp_ini_file = pytester.makefile( @@ -536,6 +627,8 @@ class TestConfigCmdlineParsing: temp_ini_file_norm = normpath(str(temp_ini_file)) ret = pytest.main(["-c", temp_ini_file_norm]) assert ret == ExitCode.OK + ret = pytest.main(["--config-file", temp_ini_file_norm]) + assert ret == ExitCode.OK class TestConfigAPI: @@ -595,20 +688,13 @@ class TestConfigAPI: def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: somepath = tmp_path.joinpath("x", "y", "z") p = tmp_path.joinpath("conftest.py") - p.write_text(f"mylist = {['.', str(somepath)]}") + p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8") config = pytester.parseconfigure(p) - assert ( - config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) - is None - ) - pl = ( - config._getconftest_pathlist("mylist", path=tmp_path, rootpath=tmp_path) - or [] - ) - print(pl) - assert len(pl) == 2 - assert pl[0] == tmp_path - assert pl[1] == somepath + assert config._getconftest_pathlist("notexist", path=tmp_path) is None + assert config._getconftest_pathlist("mylist", path=tmp_path) == [ + tmp_path, + somepath, + ] @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) def test_addini(self, pytester: Pytester, maybe_type: str) -> None: @@ -801,6 +887,67 @@ class TestConfigAPI: assert len(values) == 2 assert values == ["456", "123"] + def test_addini_default_values(self, pytester: Pytester) -> None: + """Tests the default values for configuration based on + config type + """ + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("linelist1", "", type="linelist") + parser.addini("paths1", "", type="paths") + parser.addini("pathlist1", "", type="pathlist") + parser.addini("args1", "", type="args") + parser.addini("bool1", "", type="bool") + parser.addini("string1", "", type="string") + parser.addini("none_1", "", type="linelist", default=None) + parser.addini("none_2", "", default=None) + parser.addini("no_type", "") + """ + ) + + config = pytester.parseconfig() + # default for linelist, paths, pathlist and args is [] + value = config.getini("linelist1") + assert value == [] + value = config.getini("paths1") + assert value == [] + value = config.getini("pathlist1") + assert value == [] + value = config.getini("args1") + assert value == [] + # default for bool is False + value = config.getini("bool1") + assert value is False + # default for string is "" + value = config.getini("string1") + assert value == "" + # should return None if None is explicity set as default value + # irrespective of the type argument + value = config.getini("none_1") + assert value is None + value = config.getini("none_2") + assert value is None + # in case no type is provided and no default set + # treat it as string and default value will be "" + value = config.getini("no_type") + assert value == "" + + @pytest.mark.parametrize( + "type, expected", + [ + pytest.param(None, "", id="None"), + pytest.param("string", "", id="string"), + pytest.param("paths", [], id="paths"), + pytest.param("pathlist", [], id="pathlist"), + pytest.param("args", [], id="args"), + pytest.param("linelist", [], id="linelist"), + pytest.param("bool", False, id="bool"), + ], + ) + def test_get_ini_default_for_type(self, type: Any, expected: Any) -> None: + assert get_ini_default_for_type(type) == expected + def test_confcutdir_check_isdir(self, pytester: Pytester) -> None: """Give an error if --confcutdir is not a valid directory (#2078)""" exp_match = r"^--confcutdir must be a directory, given: " @@ -827,6 +974,9 @@ class TestConfigAPI: (["src/bar/__init__.py"], ["bar"]), (["src/bar/__init__.py", "setup.py"], ["bar"]), (["source/python/bar/__init__.py", "setup.py"], ["bar"]), + # editable installation finder modules + (["__editable___xyz_finder.py"], []), + (["bar/__init__.py", "__editable___xyz_finder.py"], ["bar"]), ], ) def test_iter_rewritable_modules(self, names, expected) -> None: @@ -868,7 +1018,8 @@ class TestConfigFromdictargs: [pytest] name = value """ - ) + ), + encoding="utf-8", ) inifilename = "../../foo/bar.ini" @@ -885,7 +1036,8 @@ class TestConfigFromdictargs: name = wrong-value should_not_be_set = true """ - ) + ), + encoding="utf-8", ) with MonkeyPatch.context() as mp: mp.chdir(cwd) @@ -953,7 +1105,7 @@ def test_preparse_ordering_with_setuptools( def my_dists(): return (Dist,) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) pytester.makeconftest( """ pytest_plugins = "mytestplugin", @@ -986,7 +1138,7 @@ def test_setuptools_importerror_issue1479( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) with pytest.raises(ImportError): pytester.parseconfig() @@ -1013,7 +1165,7 @@ def test_importlib_metadata_broken_distribution( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) pytester.parseconfig() @@ -1041,7 +1193,7 @@ def test_plugin_preparse_prevents_setuptools_loading( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") @@ -1090,8 +1242,8 @@ def test_disable_plugin_autoload( return (Distribution(),) monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - monkeypatch.setattr(importlib_metadata, "distributions", distributions) - monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] + monkeypatch.setattr(importlib.metadata, "distributions", distributions) + monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None assert has_loaded == should_load @@ -1109,8 +1261,7 @@ def test_plugin_loading_order(pytester: Pytester) -> None: import myplugin assert myplugin.terminal_plugin == [False, True] """, - **{ - "myplugin": """ + myplugin=""" terminal_plugin = [] def pytest_configure(config): @@ -1119,25 +1270,13 @@ def test_plugin_loading_order(pytester: Pytester) -> None: def pytest_sessionstart(session): config = session.config terminal_plugin.append(bool(config.pluginmanager.get_plugin("terminalreporter"))) - """ - }, + """, ) pytester.syspathinsert() result = pytester.runpytest("-p", "myplugin", str(p1)) assert result.ret == 0 -def test_cmdline_processargs_simple(pytester: Pytester) -> None: - pytester.makeconftest( - """ - def pytest_cmdline_preparse(args): - args.append("-h") - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines(["*pytest*", "*-h*"]) - - def test_invalid_options_show_extra_information(pytester: Pytester) -> None: """Display extra information when pytest exits due to unrecognized options in the command-line.""" @@ -1267,7 +1406,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test): hookimpls = [ ( hookimpl.function.__module__, - "wrapper" if hookimpl.hookwrapper else "nonwrapper", + "wrapper" if (hookimpl.wrapper or hookimpl.hookwrapper) else "nonwrapper", ) for hookimpl in hc.get_hookimpls() ] @@ -1321,16 +1460,16 @@ def test_collect_pytest_prefix_bug(pytestconfig): class TestRootdir: def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - assert get_common_ancestor([tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [tmp_path]) == tmp_path a = tmp_path / "a" a.mkdir() - assert get_common_ancestor([a, tmp_path]) == tmp_path - assert get_common_ancestor([tmp_path, a]) == tmp_path + assert get_common_ancestor(Path.cwd(), [a, tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [tmp_path, a]) == tmp_path monkeypatch.chdir(tmp_path) - assert get_common_ancestor([]) == tmp_path + assert get_common_ancestor(Path.cwd(), []) == tmp_path no_path = tmp_path / "does-not-exist" - assert get_common_ancestor([no_path]) == tmp_path - assert get_common_ancestor([no_path / "a"]) == tmp_path + assert get_common_ancestor(Path.cwd(), [no_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [no_path / "a"]) == tmp_path @pytest.mark.parametrize( "name, contents", @@ -1345,17 +1484,27 @@ class TestRootdir: ) def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: inipath = tmp_path / name - inipath.write_text(contents, "utf-8") + inipath.write_text(contents, encoding="utf-8") a = tmp_path / "a" a.mkdir() b = a / "b" b.mkdir() for args in ([str(tmp_path)], [str(a)], [str(b)]): - rootpath, parsed_inipath, _ = determine_setup(None, args) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=args, + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath - rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)]) + rootpath, parsed_inipath, ini_config = determine_setup( + inifile=None, + args=[str(b), str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath assert ini_config == {"x": "10"} @@ -1367,7 +1516,12 @@ class TestRootdir: a = tmp_path / "a" a.mkdir() (a / name).touch() - rootpath, parsed_inipath, _ = determine_setup(None, [str(a)]) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=[str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath @@ -1376,14 +1530,24 @@ class TestRootdir: a.mkdir() (a / "setup.cfg").touch() (tmp_path / "setup.py").touch() - rootpath, inipath, inicfg = determine_setup(None, [str(a)]) + rootpath, inipath, inicfg = determine_setup( + inifile=None, + args=[str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)]) + rootpath, inipath, inicfg = determine_setup( + inifile=None, + args=[str(tmp_path)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} @@ -1404,8 +1568,13 @@ class TestRootdir: ) -> None: p = tmp_path / name p.touch() - p.write_text(contents, "utf-8") - rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) + p.write_text(contents, encoding="utf-8") + rootpath, inipath, ini_config = determine_setup( + inifile=str(p), + args=[str(tmp_path)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath == p assert ini_config == {"x": "10"} @@ -1419,14 +1588,24 @@ class TestRootdir: monkeypatch.chdir(tmp_path) # No config file is explicitly given: rootdir is determined to be cwd. - rootpath, found_inipath, *_ = determine_setup(None, [str(tests_dir)]) + rootpath, found_inipath, *_ = determine_setup( + inifile=None, + args=[str(tests_dir)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert found_inipath is None # Config file is explicitly given: rootdir is determined to be inifile's directory. inipath = tmp_path / "pytest.ini" inipath.touch() - rootpath, found_inipath, *_ = determine_setup(str(inipath), [str(tests_dir)]) + rootpath, found_inipath, *_ = determine_setup( + inifile=str(inipath), + args=[str(tests_dir)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert found_inipath == inipath @@ -1438,7 +1617,12 @@ class TestRootdir: a.mkdir() b = tmp_path / "b" b.mkdir() - rootpath, inifile, _ = determine_setup(None, [str(a), str(b)]) + rootpath, inifile, _ = determine_setup( + inifile=None, + args=[str(a), str(b)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inifile is None @@ -1449,7 +1633,12 @@ class TestRootdir: b.mkdir() inipath = a / "pytest.ini" inipath.touch() - rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)]) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=[str(a), str(b)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == a assert inipath == parsed_inipath @@ -1458,7 +1647,12 @@ class TestRootdir: self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup(None, dirs) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=dirs, + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None @@ -1469,7 +1663,12 @@ class TestRootdir: a.mkdir() (a / "exists").touch() monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup(None, ["a/exist"]) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=["a/exist"], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None @@ -1483,7 +1682,12 @@ class TestRootdir: (tmp_path / "myproject" / "tests").mkdir() monkeypatch.chdir(tmp_path / "myproject") - rootpath, inipath, _ = determine_setup(None, ["tests/"]) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=["tests/"], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path / "myproject" assert inipath == tmp_path / "myproject" / "setup.cfg" @@ -1495,12 +1699,11 @@ class TestOverrideIniArgs: section = "[pytest]" if name != "setup.cfg" else "[tool:pytest]" pytester.path.joinpath(name).write_text( textwrap.dedent( - """ + f""" {section} - custom = 1.0""".format( - section=section - ) - ) + custom = 1.0""" + ), + encoding="utf-8", ) pytester.makeconftest( """ @@ -1537,7 +1740,7 @@ class TestOverrideIniArgs: ) pytester.makepyfile( r""" - def test_overriden(pytestconfig): + def test_overridden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: @@ -1663,8 +1866,8 @@ class TestOverrideIniArgs: result = pytester.runpytest("cache_dir=ignored") result.stderr.fnmatch_lines( [ - "%s: error: argument -o/--override-ini: expected one argument (via addopts config)" - % (pytester._request.config._parser.optparser.prog,) + f"{pytester._request.config._parser.optparser.prog}: error: " + f"argument -o/--override-ini: expected one argument (via addopts config)" ] ) assert result.ret == _pytest.config.ExitCode.USAGE_ERROR @@ -1701,6 +1904,18 @@ class TestOverrideIniArgs: assert "ERROR:" not in result.stderr.str() result.stdout.fnmatch_lines(["collected 1 item", "*= 1 passed in *="]) + def test_override_ini_without_config_file(self, pytester: Pytester) -> None: + pytester.makepyfile(**{"src/override_ini_without_config_file.py": ""}) + pytester.makepyfile( + **{ + "tests/test_override_ini_without_config_file.py": ( + "import override_ini_without_config_file\ndef test(): pass" + ), + } + ) + result = pytester.runpytest("--override-ini", "pythonpath=src") + assert result.parseoutcomes() == {"passed": 1} + def test_help_via_addopts(pytester: Pytester) -> None: pytester.makeini( @@ -1752,8 +1967,8 @@ def test_help_and_version_after_argument_error(pytester: Pytester) -> None: result.stderr.fnmatch_lines( [ "ERROR: usage: *", - "%s: error: argument --invalid-option-should-allow-for-help: expected one argument" - % (pytester._request.config._parser.optparser.prog,), + f"{pytester._request.config._parser.optparser.prog}: error: " + f"argument --invalid-option-should-allow-for-help: expected one argument", ] ) # Does not display full/default help. @@ -1791,6 +2006,10 @@ def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> No result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) assert result.ret == ExitCode.USAGE_ERROR + result = pytester.runpytest(str(p), "-p no:capture", "-s") + result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"]) + assert result.ret == ExitCode.USAGE_ERROR + def test_invocation_args(pytester: Pytester) -> None: """Ensure that Config.invocation_* arguments are correctly defined""" @@ -1828,16 +2047,6 @@ def test_invocation_args(pytester: Pytester) -> None: ], ) def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None: - if plugin == "debugging": - # Fixed in xdist (after 1.27.0). - # https://github.com/pytest-dev/pytest-xdist/pull/422 - try: - import xdist # noqa: F401 - except ImportError: - pass - else: - pytest.skip("does not work with xdist currently") - p = pytester.makepyfile("def test(): pass") result = pytester.runpytest(str(p), "-pno:%s" % plugin) @@ -1846,7 +2055,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None result.stderr.fnmatch_lines( [ "ERROR: not found: */test_config_blocked_default_plugins.py", - "(no name '*/test_config_blocked_default_plugins.py' in any of [])", + "(no match in any of *<Dir *>*", ] ) return @@ -1887,6 +2096,9 @@ class TestSetupCfg: with pytest.raises(pytest.fail.Exception): pytester.runpytest("-c", "custom.cfg") + with pytest.raises(pytest.fail.Exception): + pytester.runpytest("--config-file", "custom.cfg") + class TestPytestPluginsVariable: def test_pytest_plugins_in_non_top_level_conftest_unsupported( @@ -1915,7 +2127,6 @@ class TestPytestPluginsVariable: self, pytester: Pytester, use_pyargs: bool ) -> None: """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" - files = { "src/pkg/__init__.py": "", "src/pkg/conftest.py": "", @@ -1930,9 +2141,7 @@ class TestPytestPluginsVariable: args = ("--pyargs", "pkg") if use_pyargs else () res = pytester.runpytest(*args) assert res.ret == (0 if use_pyargs else 2) - msg = ( - msg - ) = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" if use_pyargs: assert msg not in res.stdout.str() else: @@ -1995,9 +2204,7 @@ def test_conftest_import_error_repr(tmp_path: Path) -> None: try: raise RuntimeError("some error") except Exception as exc: - assert exc.__traceback__ is not None - exc_info = (type(exc), exc, exc.__traceback__) - raise ConftestImportFailure(path, exc_info) from exc + raise ConftestImportFailure(path, cause=exc) from exc def test_strtobool() -> None: @@ -2108,8 +2315,81 @@ class TestDebugOptions: result = pytester.runpytest("-h") result.stdout.fnmatch_lines( [ - "*store internal tracing debug information in this log*", - "*This file is opened with 'w' and truncated as a result*", - "*Defaults to 'pytestdebug.log'.", + "*Store internal tracing debug information in this log*", + "*file. This file is opened with 'w' and truncated as a*", + "*Default: pytestdebug.log.", ] ) + + +class TestVerbosity: + SOME_OUTPUT_TYPE = Config.VERBOSITY_ASSERTIONS + SOME_OUTPUT_VERBOSITY_LEVEL = 5 + + class VerbosityIni: + def pytest_addoption(self, parser: Parser) -> None: + Config._add_verbosity_ini( + parser, TestVerbosity.SOME_OUTPUT_TYPE, help="some help text" + ) + + def test_level_matches_verbose_when_not_specified( + self, pytester: Pytester, tmp_path: Path + ) -> None: + tmp_path.joinpath("pytest.ini").write_text( + textwrap.dedent( + """\ + [pytest] + addopts = --verbose + """ + ), + encoding="utf-8", + ) + pytester.plugins = [TestVerbosity.VerbosityIni()] + + config = pytester.parseconfig(tmp_path) + + assert ( + config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) + == config.option.verbose + ) + + def test_level_matches_verbose_when_not_known_type( + self, pytester: Pytester, tmp_path: Path + ) -> None: + tmp_path.joinpath("pytest.ini").write_text( + textwrap.dedent( + """\ + [pytest] + addopts = --verbose + """ + ), + encoding="utf-8", + ) + pytester.plugins = [TestVerbosity.VerbosityIni()] + + config = pytester.parseconfig(tmp_path) + + assert config.get_verbosity("some fake verbosity type") == config.option.verbose + + def test_level_matches_specified_override( + self, pytester: Pytester, tmp_path: Path + ) -> None: + setting_name = f"verbosity_{TestVerbosity.SOME_OUTPUT_TYPE}" + tmp_path.joinpath("pytest.ini").write_text( + textwrap.dedent( + f"""\ + [pytest] + addopts = --verbose + {setting_name} = {TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL} + """ + ), + encoding="utf-8", + ) + pytester.plugins = [TestVerbosity.VerbosityIni()] + + config = pytester.parseconfig(tmp_path) + + assert ( + config.get_verbosity(TestVerbosity.SOME_OUTPUT_TYPE) + == TestVerbosity.SOME_OUTPUT_VERBOSITY_LEVEL + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_conftest.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_conftest.py index 64c1014a533..06438f082a9 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_conftest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_conftest.py @@ -1,20 +1,22 @@ -import argparse +# mypy: allow-untyped-defs import os -import textwrap from pathlib import Path +import textwrap from typing import cast from typing import Dict from typing import Generator from typing import List from typing import Optional +from typing import Sequence +from typing import Union -import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester from _pytest.tmpdir import TempPathFactory +import pytest def ConftestWithSetinitial(path) -> PytestPluginManager: @@ -24,18 +26,20 @@ def ConftestWithSetinitial(path) -> PytestPluginManager: def conftest_setinitial( - conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None + conftest: PytestPluginManager, + args: Sequence[Union[str, Path]], + confcutdir: Optional[Path] = None, ) -> None: - class Namespace: - def __init__(self) -> None: - self.file_or_dir = args - self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None - self.noconftest = False - self.pyargs = False - self.importmode = "prepend" - - namespace = cast(argparse.Namespace, Namespace()) - conftest._set_initial_conftests(namespace, rootpath=Path(args[0])) + conftest._set_initial_conftests( + args=args, + pyargs=False, + noconftest=False, + rootpath=Path(args[0]), + confcutdir=confcutdir, + invocation_dir=Path.cwd(), + importmode="prepend", + consider_namespace_packages=False, + ) @pytest.mark.usefixtures("_sys_snapshot") @@ -46,8 +50,12 @@ class TestConftestValueAccessGlobal: ) -> Generator[Path, None, None]: tmp_path = tmp_path_factory.mktemp("basedir", numbered=True) tmp_path.joinpath("adir/b").mkdir(parents=True) - tmp_path.joinpath("adir/conftest.py").write_text("a=1 ; Directory = 3") - tmp_path.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path.joinpath("adir/conftest.py").write_text( + "a=1 ; Directory = 3", encoding="utf-8" + ) + tmp_path.joinpath("adir/b/conftest.py").write_text( + "b=2 ; a = 1.5", encoding="utf-8" + ) if request.param == "inpackage": tmp_path.joinpath("adir/__init__.py").touch() tmp_path.joinpath("adir/b/__init__.py").touch() @@ -57,63 +65,69 @@ class TestConftestValueAccessGlobal: def test_basic_init(self, basedir: Path) -> None: conftest = PytestPluginManager() p = basedir / "adir" - assert ( - conftest._rget_with_confmod("a", p, importmode="prepend", rootpath=basedir)[ - 1 - ] - == 1 + conftest._loadconftestmodules( + p, importmode="prepend", rootpath=basedir, consider_namespace_packages=False ) + assert conftest._rget_with_confmod("a", p)[1] == 1 - def test_immediate_initialiation_and_incremental_are_the_same( + def test_immediate_initialization_and_incremental_are_the_same( self, basedir: Path ) -> None: conftest = PytestPluginManager() assert not len(conftest._dirpath2confmods) - conftest._getconftestmodules( - basedir, importmode="prepend", rootpath=Path(basedir) + conftest._loadconftestmodules( + basedir, + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) snap1 = len(conftest._dirpath2confmods) assert snap1 == 1 - conftest._getconftestmodules( - basedir / "adir", importmode="prepend", rootpath=basedir + conftest._loadconftestmodules( + basedir / "adir", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 1 - conftest._getconftestmodules( - basedir / "b", importmode="prepend", rootpath=basedir + conftest._loadconftestmodules( + basedir / "b", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): - conftest._rget_with_confmod( - "a", basedir, importmode="prepend", rootpath=Path(basedir) - ) + conftest._rget_with_confmod("a", basedir) def test_value_access_by_path(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) adir = basedir / "adir" - assert ( - conftest._rget_with_confmod( - "a", adir, importmode="prepend", rootpath=basedir - )[1] - == 1 + conftest._loadconftestmodules( + adir, + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) - assert ( - conftest._rget_with_confmod( - "a", adir / "b", importmode="prepend", rootpath=basedir - )[1] - == 1.5 + assert conftest._rget_with_confmod("a", adir)[1] == 1 + conftest._loadconftestmodules( + adir / "b", + importmode="prepend", + rootpath=basedir, + consider_namespace_packages=False, ) + assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5 def test_value_access_with_confmod(self, basedir: Path) -> None: startdir = basedir / "adir" / "b" startdir.joinpath("xx").mkdir() conftest = ConftestWithSetinitial(startdir) - mod, value = conftest._rget_with_confmod( - "a", startdir, importmode="prepend", rootpath=Path(basedir) - ) + mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 + assert mod.__file__ is not None path = Path(mod.__file__) assert path.parent == basedir / "adir" / "b" assert path.stem == "conftest" @@ -121,8 +135,12 @@ class TestConftestValueAccessGlobal: def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: tmp_path.joinpath("adir-1.0/b").mkdir(parents=True) - tmp_path.joinpath("adir-1.0/conftest.py").write_text("a=1 ; Directory = 3") - tmp_path.joinpath("adir-1.0/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path.joinpath("adir-1.0/conftest.py").write_text( + "a=1 ; Directory = 3", encoding="utf-8" + ) + tmp_path.joinpath("adir-1.0/b/conftest.py").write_text( + "b=2 ; a = 1.5", encoding="utf-8" + ) tmp_path.joinpath("adir-1.0/b/__init__.py").touch() tmp_path.joinpath("adir-1.0/__init__.py").touch() ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) @@ -133,9 +151,7 @@ def test_doubledash_considered(pytester: Pytester) -> None: conf.joinpath("conftest.py").touch() conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.name, conf.name]) - values = conftest._getconftestmodules( - conf, importmode="prepend", rootpath=pytester.path - ) + values = conftest._getconftestmodules(conf) assert len(values) == 1 @@ -145,10 +161,9 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None: p = pytester.mkdir(name) p.joinpath("conftest.py").touch() - conftest = PytestPluginManager() - conftest_setinitial(conftest, names) - d = list(conftest._conftestpath2mod.values()) - assert len(d) == len(names) + pm = PytestPluginManager() + conftest_setinitial(pm, names) + assert len(set(pm.get_plugins()) - {pm}) == len(names) def test_conftest_global_import(pytester: Pytester) -> None: @@ -159,15 +174,25 @@ def test_conftest_global_import(pytester: Pytester) -> None: import pytest from _pytest.config import PytestPluginManager conf = PytestPluginManager() - mod = conf._importconftest(Path("conftest.py"), importmode="prepend", rootpath=Path.cwd()) + mod = conf._importconftest( + Path("conftest.py"), + importmode="prepend", + rootpath=Path.cwd(), + consider_namespace_packages=False, + ) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) sub = Path("sub") sub.mkdir() subconf = sub / "conftest.py" - subconf.write_text("y=4") - mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd()) + subconf.write_text("y=4", encoding="utf-8") + mod2 = conf._importconftest( + subconf, + importmode="prepend", + rootpath=Path.cwd(), + consider_namespace_packages=False, + ) assert mod != mod2 assert mod2.y == 4 import conftest @@ -183,26 +208,37 @@ def test_conftestcutdir(pytester: Pytester) -> None: p = pytester.mkdir("x") conftest = PytestPluginManager() conftest_setinitial(conftest, [pytester.path], confcutdir=p) - values = conftest._getconftestmodules( - p, importmode="prepend", rootpath=pytester.path + conftest._loadconftestmodules( + p, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) + values = conftest._getconftestmodules(p) assert len(values) == 0 - values = conftest._getconftestmodules( - conf.parent, importmode="prepend", rootpath=pytester.path + conftest._loadconftestmodules( + conf.parent, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) + values = conftest._getconftestmodules(conf.parent) assert len(values) == 0 - assert Path(conf) not in conftest._conftestpath2mod + assert not conftest.has_plugin(str(conf)) # but we can still import a conftest directly - conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path) - values = conftest._getconftestmodules( - conf.parent, importmode="prepend", rootpath=pytester.path + conftest._importconftest( + conf, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) + values = conftest._getconftestmodules(conf.parent) + assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) # and all sub paths get updated properly - values = conftest._getconftestmodules( - p, importmode="prepend", rootpath=pytester.path - ) + values = conftest._getconftestmodules(p) assert len(values) == 1 + assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) @@ -210,10 +246,9 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None: conf = pytester.makeconftest("") conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent) - values = conftest._getconftestmodules( - conf.parent, importmode="prepend", rootpath=pytester.path - ) + values = conftest._getconftestmodules(conf.parent) assert len(values) == 1 + assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) @@ -222,15 +257,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: sub = pytester.mkdir(name) subconftest = sub.joinpath("conftest.py") subconftest.touch() - conftest = PytestPluginManager() - conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path) + pm = PytestPluginManager() + conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path) key = subconftest.resolve() if name not in ("whatever", ".dotdir"): - assert key in conftest._conftestpath2mod - assert len(conftest._conftestpath2mod) == 1 + assert pm.has_plugin(str(key)) + assert len(set(pm.get_plugins()) - {pm}) == 1 else: - assert key not in conftest._conftestpath2mod - assert len(conftest._conftestpath2mod) == 0 + assert not pm.has_plugin(str(key)) + assert len(set(pm.get_plugins()) - {pm}) == 0 def test_conftest_confcutdir(pytester: Pytester) -> None: @@ -242,13 +277,45 @@ def test_conftest_confcutdir(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.no_fnmatch_line("*warning: could not load initial*") +def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) -> None: + """When using `--pyargs` to run tests in an installed packages (located e.g. + in a site-packages in the PYTHONPATH), conftest files in there are picked + up. + + Regression test for #9767. + """ + # pytester dir - the source tree. + # tmp_path - the simulated site-packages dir (not in source tree). + + pytester.syspathinsert(tmp_path) + pytester.makepyprojecttoml("[tool.pytest.ini_options]") + tmp_path.joinpath("foo").mkdir() + tmp_path.joinpath("foo", "__init__.py").touch() + tmp_path.joinpath("foo", "conftest.py").write_text( + textwrap.dedent( + """\ + import pytest + @pytest.fixture + def fix(): return None + """ + ), + encoding="utf-8", + ) + tmp_path.joinpath("foo", "test_it.py").write_text( + "def test_it(fix): pass", encoding="utf-8" + ) + result = pytester.runpytest("--pyargs", "foo") + assert result.ret == 0 + + def test_conftest_symlink(pytester: Pytester) -> None: """`conftest.py` discovery follows normal path resolution and does not resolve symlinks.""" # Structure: @@ -329,7 +396,7 @@ def test_conftest_symlink_files(pytester: Pytester) -> None: @pytest.mark.skipif( os.path.normcase("x") != os.path.normcase("X"), - reason="only relevant for case insensitive file systems", + reason="only relevant for case-insensitive file systems", ) def test_conftest_badcase(pytester: Pytester) -> None: """Check conftest.py loading when directory casing is wrong (#5792).""" @@ -369,7 +436,8 @@ def test_conftest_existing_junitxml(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ - ) + ), + encoding="utf-8", ) pytester.makefile(ext=".xml", junit="") # Writes junit.xml result = pytester.runpytest("-h", "--junitxml", "junit.xml") @@ -380,18 +448,21 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> ct1 = pytester.makeconftest("") sub = pytester.mkdir("sub") ct2 = sub / "conftest.py" - ct2.write_text("") + ct2.write_text("", encoding="utf-8") - def impct(p, importmode, root): + def impct(p, importmode, root, consider_namespace_packages): return p conftest = PytestPluginManager() conftest._confcutdir = pytester.path monkeypatch.setattr(conftest, "_importconftest", impct) - mods = cast( - List[Path], - conftest._getconftestmodules(sub, importmode="prepend", rootpath=pytester.path), + conftest._loadconftestmodules( + sub, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) + mods = cast(List[Path], conftest._getconftestmodules(sub)) expected = [ct1, ct2] assert mods == expected @@ -418,7 +489,8 @@ def test_fixture_dependency(pytester: Pytester) -> None: def bar(foo): return 'bar' """ - ) + ), + encoding="utf-8", ) subsub = sub.joinpath("subsub") subsub.mkdir() @@ -435,7 +507,8 @@ def test_fixture_dependency(pytester: Pytester) -> None: def test_event_fixture(bar): assert bar == 'sub bar' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("sub") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -449,10 +522,11 @@ def test_conftest_found_with_double_dash(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--hello-world", action="store_true") """ - ) + ), + encoding="utf-8", ) p = sub.joinpath("test_hello.py") - p.write_text("def test_hello(): pass") + p.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest(str(p) + "::test_hello", "-h") result.stdout.fnmatch_lines( """ @@ -476,7 +550,8 @@ class TestConftestVisibility: def fxtr(): return "from-package" """ - ) + ), + encoding="utf-8", ) package.joinpath("test_pkgroot.py").write_text( textwrap.dedent( @@ -484,7 +559,8 @@ class TestConftestVisibility: def test_pkgroot(fxtr): assert fxtr == "from-package" """ - ) + ), + encoding="utf-8", ) swc = package.joinpath("swc") @@ -498,7 +574,8 @@ class TestConftestVisibility: def fxtr(): return "from-swc" """ - ) + ), + encoding="utf-8", ) swc.joinpath("test_with_conftest.py").write_text( textwrap.dedent( @@ -506,7 +583,8 @@ class TestConftestVisibility: def test_with_conftest(fxtr): assert fxtr == "from-swc" """ - ) + ), + encoding="utf-8", ) snc = package.joinpath("snc") @@ -519,10 +597,11 @@ class TestConftestVisibility: assert fxtr == "from-package" # No local conftest.py, so should # use value from parent dir's """ - ) + ), + encoding="utf-8", ) print("created directory structure:") - for x in pytester.path.rglob(""): + for x in pytester.path.glob("**/"): print(" " + str(x.relative_to(pytester.path))) return {"runner": runner, "package": package, "swc": swc, "snc": snc} @@ -563,7 +642,13 @@ class TestConftestVisibility: print("pytestarg : %s" % testarg) print("expected pass : %s" % expect_ntests_passed) os.chdir(dirs[chdir]) - reprec = pytester.inline_run(testarg, "-q", "--traceconfig") + reprec = pytester.inline_run( + testarg, + "-q", + "--traceconfig", + "--confcutdir", + pytester.path, + ) reprec.assertoutcome(passed=expect_ntests_passed) @@ -579,7 +664,7 @@ def test_search_conftest_up_to_inifile( root = pytester.path src = root.joinpath("src") src.mkdir() - src.joinpath("pytest.ini").write_text("[pytest]") + src.joinpath("pytest.ini").write_text("[pytest]", encoding="utf-8") src.joinpath("conftest.py").write_text( textwrap.dedent( """\ @@ -587,7 +672,8 @@ def test_search_conftest_up_to_inifile( @pytest.fixture def fix1(): pass """ - ) + ), + encoding="utf-8", ) src.joinpath("test_foo.py").write_text( textwrap.dedent( @@ -597,7 +683,8 @@ def test_search_conftest_up_to_inifile( def test_2(out_of_reach): pass """ - ) + ), + encoding="utf-8", ) root.joinpath("conftest.py").write_text( textwrap.dedent( @@ -606,7 +693,8 @@ def test_search_conftest_up_to_inifile( @pytest.fixture def out_of_reach(): pass """ - ) + ), + encoding="utf-8", ) args = [str(src)] @@ -689,7 +777,8 @@ def test_required_option_help(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true", required=True) """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-h", x) result.stdout.no_fnmatch_line("*argument --xyz is required*") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_debugging.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_debugging.py index a822bb57f58..7582dac6742 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_debugging.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_debugging.py @@ -1,23 +1,12 @@ -import os +# mypy: allow-untyped-defs import sys from typing import List import _pytest._code -import pytest from _pytest.debugging import _validate_usepdb_cls from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester - -try: - # Type ignored for Python <= 3.6. - breakpoint # type: ignore -except NameError: - SUPPORTS_BREAKPOINT_BUILTIN = False -else: - SUPPORTS_BREAKPOINT_BUILTIN = True - - -_ENVIRON_PYTHONBREAKPOINT = os.environ.get("PYTHONBREAKPOINT", "") +import pytest @pytest.fixture(autouse=True) @@ -28,10 +17,19 @@ def pdb_env(request): pytester._monkeypatch.setenv("PDBPP_HIJACK_PDB", "0") -def runpdb_and_get_report(pytester: Pytester, source: str): +def runpdb(pytester: Pytester, source: str): p = pytester.makepyfile(source) - result = pytester.runpytest_inprocess("--pdb", p) - reports = result.reprec.getreports("pytest_runtest_logreport") # type: ignore[attr-defined] + return pytester.runpytest_inprocess("--pdb", p) + + +def runpdb_and_get_stdout(pytester: Pytester, source: str): + result = runpdb(pytester, source) + return result.stdout.str() + + +def runpdb_and_get_report(pytester: Pytester, source: str): + result = runpdb(pytester, source) + reports = result.reprec.getreports("pytest_runtest_logreport") assert len(reports) == 3, reports # setup/call/teardown return reports[1] @@ -132,6 +130,16 @@ class TestPDB: assert rep.skipped assert len(pdblist) == 0 + def test_pdb_on_top_level_raise_skiptest(self, pytester, pdblist) -> None: + stdout = runpdb_and_get_stdout( + pytester, + """ + import unittest + raise unittest.SkipTest("This is a common way to skip an entire file.") + """, + ) + assert "entering PDB" not in stdout, stdout + def test_pdb_on_BdbQuit(self, pytester, pdblist) -> None: rep = runpdb_and_get_report( pytester, @@ -252,7 +260,7 @@ class TestPDB: """ def test_1(): import logging - logging.warn("get " + "rekt") + logging.warning("get " + "rekt") assert False """ ) @@ -271,7 +279,7 @@ class TestPDB: """ def test_1(): import logging - logging.warn("get " + "rekt") + logging.warning("get " + "rekt") assert False """ ) @@ -361,6 +369,7 @@ class TestPDB: result = pytester.runpytest_subprocess("--pdb", ".") result.stdout.fnmatch_lines(["-> import unknown"]) + @pytest.mark.xfail(reason="#10042", strict=False) def test_pdb_interaction_capturing_simple(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -529,6 +538,7 @@ class TestPDB: assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest + @pytest.mark.xfail(reason="#10042", strict=False) def test_pdb_interaction_capturing_twice(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -564,6 +574,7 @@ class TestPDB: assert "1 failed" in rest self.flush(child) + @pytest.mark.xfail(reason="#10042", strict=False) def test_pdb_with_injected_do_debug(self, pytester: Pytester) -> None: """Simulates pdbpp, which injects Pdb into do_debug, and uses self.__class__ in do_continue. @@ -911,14 +922,6 @@ class TestPDB: class TestDebuggingBreakpoints: - def test_supports_breakpoint_module_global(self) -> None: - """Test that supports breakpoint global marks on Python 3.7+.""" - if sys.version_info >= (3, 7): - assert SUPPORTS_BREAKPOINT_BUILTIN is True - - @pytest.mark.skipif( - not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" - ) @pytest.mark.parametrize("arg", ["--pdb", ""]) def test_sys_breakpointhook_configure_and_unconfigure( self, pytester: Pytester, arg: str @@ -952,10 +955,10 @@ class TestDebuggingBreakpoints: result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) - @pytest.mark.skipif( - not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" - ) - def test_pdb_custom_cls(self, pytester: Pytester, custom_debugger_hook) -> None: + def test_pdb_custom_cls( + self, pytester: Pytester, custom_debugger_hook, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.delenv("PYTHONBREAKPOINT", raising=False) p1 = pytester.makepyfile( """ def test_nothing(): @@ -969,9 +972,6 @@ class TestDebuggingBreakpoints: assert custom_debugger_hook == ["init", "set_trace"] @pytest.mark.parametrize("arg", ["--pdb", ""]) - @pytest.mark.skipif( - not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" - ) def test_environ_custom_class( self, pytester: Pytester, custom_debugger_hook, arg: str ) -> None: @@ -1002,14 +1002,10 @@ class TestDebuggingBreakpoints: result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) - @pytest.mark.skipif( - not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" - ) - @pytest.mark.skipif( - not _ENVIRON_PYTHONBREAKPOINT == "", - reason="Requires breakpoint() default value", - ) - def test_sys_breakpoint_interception(self, pytester: Pytester) -> None: + def test_sys_breakpoint_interception( + self, pytester: Pytester, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.delenv("PYTHONBREAKPOINT", raising=False) p1 = pytester.makepyfile( """ def test_1(): @@ -1025,9 +1021,7 @@ class TestDebuggingBreakpoints: assert "reading from stdin while output" not in rest TestPDB.flush(child) - @pytest.mark.skipif( - not SUPPORTS_BREAKPOINT_BUILTIN, reason="Requires breakpoint() builtin" - ) + @pytest.mark.xfail(reason="#10042", strict=False) def test_pdb_not_altered(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -1128,7 +1122,7 @@ class TestTraceOption: def test_trace_after_runpytest(pytester: Pytester) -> None: - """Test that debugging's pytest_configure is re-entrant.""" + """Test that debugging's pytest_configure is reentrant.""" p1 = pytester.makepyfile( """ from _pytest.debugging import pytestPDB @@ -1159,7 +1153,7 @@ def test_trace_after_runpytest(pytester: Pytester) -> None: def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None: - """Test that debugging's pytest_configure is re-entrant.""" + """Test that debugging's pytest_configure is reentrant.""" p1 = pytester.makepyfile( """ def call_pdb_set_trace(): @@ -1187,10 +1181,11 @@ def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None: @pytest.mark.parametrize("fixture", ("capfd", "capsys")) +@pytest.mark.xfail(reason="#10042", strict=False) def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> None: """Using "-s" with pytest should suspend/resume fixture capturing.""" p1 = pytester.makepyfile( - """ + f""" def test_inner({fixture}): import sys @@ -1205,9 +1200,7 @@ def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> Non out, err = {fixture}.readouterr() assert out =="out_inner_before\\nout_inner_after\\n" assert err =="err_inner_before\\nerr_inner_after\\n" - """.format( - fixture=fixture - ) + """ ) child = pytester.spawn_pytest(str(p1) + " -s") @@ -1280,7 +1273,6 @@ def test_pdbcls_via_local_module(pytester: Pytester) -> None: def test_raises_bdbquit_with_eoferror(pytester: Pytester) -> None: """It is not guaranteed that DontReadFromInput's read is called.""" - p1 = pytester.makepyfile( """ def input_without_read(*args, **kwargs): diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_doctest.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_doctest.py index 67b8ccdb7ec..a95dde9a6bf 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_doctest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_doctest.py @@ -1,11 +1,11 @@ +# mypy: allow-untyped-defs import inspect +from pathlib import Path import sys import textwrap -from pathlib import Path from typing import Callable from typing import Optional -import pytest from _pytest.doctest import _get_checker from _pytest.doctest import _is_main_py from _pytest.doctest import _is_mocked @@ -15,6 +15,7 @@ from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile from _pytest.pytester import Pytester +import pytest class TestDoctests: @@ -113,6 +114,32 @@ class TestDoctests: reprec = pytester.inline_run(p) reprec.assertoutcome(failed=1) + def test_importmode(self, pytester: Pytester): + pytester.makepyfile( + **{ + "src/namespacepkg/innerpkg/__init__.py": "", + "src/namespacepkg/innerpkg/a.py": """ + def some_func(): + return 42 + """, + "src/namespacepkg/innerpkg/b.py": """ + from namespacepkg.innerpkg.a import some_func + def my_func(): + ''' + >>> my_func() + 42 + ''' + return some_func() + """, + } + ) + # For 'namespacepkg' to be considered a namespace package, its containing directory + # needs to be reachable from sys.path: + # https://packaging.python.org/en/latest/guides/packaging-namespace-packages + pytester.syspathinsert(pytester.path / "src") + reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib") + reprec.assertoutcome(passed=1) + def test_new_pattern(self, pytester: Pytester): p = pytester.maketxtfile( xdoc=""" @@ -160,19 +187,15 @@ class TestDoctests: def test_encoding(self, pytester, test_string, encoding): """Test support for doctest_encoding ini option.""" pytester.makeini( - """ + f""" [pytest] - doctest_encoding={} - """.format( - encoding - ) - ) - doctest = """ - >>> "{}" - {} - """.format( - test_string, repr(test_string) + doctest_encoding={encoding} + """ ) + doctest = f""" + >>> "{test_string}" + {test_string!r} + """ fn = pytester.path / "test_encoding.txt" fn.write_text(doctest, encoding=encoding) @@ -201,7 +224,7 @@ class TestDoctests: "Traceback (most recent call last):", ' File "*/doctest.py", line *, in __run', " *", - *((" *^^^^*",) if sys.version_info >= (3, 11) else ()), + *((" *^^^^*", " *", " *") if sys.version_info >= (3, 13) else ()), ' File "<doctest test_doctest_unexpected_exception.txt[1]>", line 1, in <module>', "ZeroDivisionError: division by zero", "*/test_doctest_unexpected_exception.txt:2: UnexpectedException", @@ -331,7 +354,8 @@ class TestDoctests: >>> 1/0 ''' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( @@ -357,7 +381,7 @@ class TestDoctests: "*= FAILURES =*", "*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*", "004 ", - "005 >>> Sample().some_property", + "005 *>>> Sample().some_property", "Expected:", " 'another thing'", "Got:", @@ -368,7 +392,7 @@ class TestDoctests: ] ) - def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester): + def test_doctest_no_linedata_on_overridden_property(self, pytester: Pytester): pytester.makepyfile( """ class Sample(object): @@ -386,7 +410,7 @@ class TestDoctests: result.stdout.fnmatch_lines( [ "*= FAILURES =*", - "*_ [[]doctest[]] test_doctest_no_linedata_on_overriden_property.Sample.some_property _*", + "*_ [[]doctest[]] test_doctest_no_linedata_on_overridden_property.Sample.some_property _*", "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example", "[?][?][?] >>> Sample().some_property", "Expected:", @@ -394,7 +418,7 @@ class TestDoctests: "Got:", " 'something'", "", - "*/test_doctest_no_linedata_on_overriden_property.py:None: DocTestFailure", + "*/test_doctest_no_linedata_on_overridden_property.py:None: DocTestFailure", "*= 1 failed in *", ] ) @@ -422,7 +446,8 @@ class TestDoctests: """\ import asdalsdkjaslkdjasd """ - ) + ), + encoding="utf-8", ) pytester.maketxtfile( """ @@ -454,6 +479,24 @@ class TestDoctests: reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) + def test_doctest_cached_property(self, pytester: Pytester): + p = pytester.makepyfile( + """ + import functools + + class Foo: + @functools.cached_property + def foo(self): + ''' + >>> assert False, "Tacos!" + ''' + ... + """ + ) + result = pytester.runpytest(p, "--doctest-modules") + result.assert_outcomes(failed=1) + assert "Tacos!" in result.stdout.str() + def test_doctestmodule_external_and_issue116(self, pytester: Pytester): p = pytester.mkpydir("hello") p.joinpath("__init__.py").write_text( @@ -466,7 +509,8 @@ class TestDoctests: 2 ''' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( @@ -685,7 +729,7 @@ class TestDoctests: >>> name = 'с' # not letter 'c' but instead Cyrillic 's'. 'anything' """ - ''' + ''' # noqa: RUF001 ) result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["Got nothing", "* 1 failed in*"]) @@ -803,8 +847,8 @@ class TestDoctests: """ p = pytester.makepyfile( setup=""" - from setuptools import setup, find_packages if __name__ == '__main__': + from setuptools import setup, find_packages setup(name='sample', version='0.0', description='description', @@ -834,6 +878,25 @@ class TestDoctests: result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 1 item*"]) + def test_setup_module(self, pytester: Pytester) -> None: + """Regression test for #12011 - setup_module not executed when running + with `--doctest-modules`.""" + pytester.makepyfile( + """ + CONSTANT = 0 + + def setup_module(): + global CONSTANT + CONSTANT = 1 + + def test(): + assert CONSTANT == 1 + """ + ) + result = pytester.runpytest("--doctest-modules") + assert result.ret == 0 + result.assert_outcomes(passed=1) + class TestLiterals: @pytest.mark.parametrize("config_mode", ["ini", "comment"]) @@ -854,23 +917,19 @@ class TestLiterals: comment = "#doctest: +ALLOW_UNICODE" pytester.maketxtfile( - test_doc=""" + test_doc=f""" >>> b'12'.decode('ascii') {comment} '12' - """.format( - comment=comment - ) + """ ) pytester.makepyfile( - foo=""" + foo=f""" def foo(): ''' >>> b'12'.decode('ascii') {comment} '12' ''' - """.format( - comment=comment - ) + """ ) reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) @@ -893,23 +952,19 @@ class TestLiterals: comment = "#doctest: +ALLOW_BYTES" pytester.maketxtfile( - test_doc=""" + test_doc=f""" >>> b'foo' {comment} 'foo' - """.format( - comment=comment - ) + """ ) pytester.makepyfile( - foo=""" + foo=f""" def foo(): ''' >>> b'foo' {comment} 'foo' ''' - """.format( - comment=comment - ) + """ ) reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) @@ -986,7 +1041,7 @@ class TestLiterals: comment = "#doctest: +NUMBER" pytester.maketxtfile( - test_doc=""" + test_doc=f""" Scalars: @@ -1038,9 +1093,7 @@ class TestLiterals: >>> 'abc' {comment} 'abc' >>> None {comment} - """.format( - comment=comment - ) + """ ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -1068,12 +1121,10 @@ class TestLiterals: ) def test_number_non_matches(self, pytester, expression, output): pytester.maketxtfile( - test_doc=""" + test_doc=f""" >>> {expression} #doctest: +NUMBER {output} - """.format( - expression=expression, output=output - ) + """ ) reprec = pytester.inline_run() reprec.assertoutcome(passed=0, failed=1) @@ -1210,7 +1261,6 @@ class TestDoctestSkips: class TestDoctestAutoUseFixtures: - SCOPES = ["module", "session", "class", "function"] def test_doctest_module_session_fixture(self, pytester: Pytester): @@ -1255,15 +1305,13 @@ class TestDoctestAutoUseFixtures: See #1057 and #1100. """ pytester.makeconftest( - """ + f""" import pytest @pytest.fixture(autouse=True, scope="{scope}") def auto(request): return 99 - """.format( - scope=scope - ) + """ ) pytester.makepyfile( test_1=''' @@ -1291,15 +1339,13 @@ class TestDoctestAutoUseFixtures: See #1057 and #1100. """ pytester.makeconftest( - """ + f""" import pytest @pytest.fixture(autouse={autouse}, scope="{scope}") def auto(request): return 99 - """.format( - scope=scope, autouse=autouse - ) + """ ) if use_fixture_in_doctest: pytester.maketxtfile( @@ -1325,7 +1371,7 @@ class TestDoctestAutoUseFixtures: behave as expected when requested for a doctest item. """ pytester.makeconftest( - """ + f""" import pytest @pytest.fixture(autouse=True, scope="{scope}") @@ -1337,9 +1383,7 @@ class TestDoctestAutoUseFixtures: if "{scope}" == 'function': assert request.function is None return 99 - """.format( - scope=scope - ) + """ ) pytester.maketxtfile( test_doc=""" @@ -1351,9 +1395,40 @@ class TestDoctestAutoUseFixtures: str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) + @pytest.mark.parametrize("scope", [*SCOPES, "package"]) + def test_auto_use_defined_in_same_module( + self, pytester: Pytester, scope: str + ) -> None: + """Autouse fixtures defined in the same module as the doctest get picked + up properly. -class TestDoctestNamespaceFixture: + Regression test for #11929. + """ + pytester.makepyfile( + f""" + import pytest + AUTO = "the fixture did not run" + + @pytest.fixture(autouse=True, scope="{scope}") + def auto(request): + global AUTO + AUTO = "the fixture ran" + + def my_doctest(): + '''My doctest. + + >>> my_doctest() + 'the fixture ran' + ''' + return AUTO + """ + ) + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=1) + + +class TestDoctestNamespaceFixture: SCOPES = ["module", "session", "class", "function"] @pytest.mark.parametrize("scope", SCOPES) @@ -1363,16 +1438,14 @@ class TestDoctestNamespaceFixture: simple text file doctest """ pytester.makeconftest( - """ + f""" import pytest import contextlib @pytest.fixture(autouse=True, scope="{scope}") def add_contextlib(doctest_namespace): doctest_namespace['cl'] = contextlib - """.format( - scope=scope - ) + """ ) p = pytester.maketxtfile( """ @@ -1390,16 +1463,14 @@ class TestDoctestNamespaceFixture: simple Python file docstring doctest """ pytester.makeconftest( - """ + f""" import pytest import contextlib @pytest.fixture(autouse=True, scope="{scope}") def add_contextlib(doctest_namespace): doctest_namespace['cl'] = contextlib - """.format( - scope=scope - ) + """ ) p = pytester.makepyfile( """ @@ -1502,16 +1573,14 @@ class TestDoctestReportingOption: def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester: Pytester): pytest.importorskip(mock_module) pytester.makepyfile( - """ + f""" from {mock_module} import call class Example(object): ''' >>> 1 + 1 2 ''' - """.format( - mock_module=mock_module - ) + """ ) result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1526,7 +1595,7 @@ class Broken: "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] ) def test_warning_on_unwrap_of_broken_object( - stop: Optional[Callable[[object], object]] + stop: Optional[Callable[[object], object]], ) -> None: bad_instance = Broken() assert inspect.unwrap.__module__ == "inspect" @@ -1542,7 +1611,9 @@ def test_warning_on_unwrap_of_broken_object( def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None: not_setup_py = tmp_path.joinpath("not_setup.py") - not_setup_py.write_text('from setuptools import setup; setup(name="foo")') + not_setup_py.write_text( + 'from setuptools import setup; setup(name="foo")', encoding="utf-8" + ) assert not _is_setup_py(not_setup_py) @@ -1558,7 +1629,7 @@ def test_is_setup_py_different_encoding(tmp_path: Path, mod: str) -> None: setup_py = tmp_path.joinpath("setup.py") contents = ( "# -*- coding: cp1252 -*-\n" - 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) + f'from {mod} import setup; setup(name="foo", description="€")\n' ) setup_py.write_bytes(contents.encode("cp1252")) assert _is_setup_py(setup_py) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_entry_points.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_entry_points.py index 5d003127363..68e3a8a92e4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_entry_points.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_entry_points.py @@ -1,7 +1,8 @@ -from _pytest.compat import importlib_metadata +# mypy: allow-untyped-defs +import importlib.metadata def test_pytest_entry_points_are_identical(): - dist = importlib_metadata.distribution("pytest") + dist = importlib.metadata.distribution("pytest") entry_map = {ep.name: ep for ep in dist.entry_points} assert entry_map["pytest"].value == entry_map["py.test"].value diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_error_diffs.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_error_diffs.py index 1668e929ab4..f290eb1679f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_error_diffs.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_error_diffs.py @@ -4,10 +4,9 @@ Tests and examples for correct "+/-" usage in error diffs. See https://github.com/pytest-dev/pytest/issues/3333 for details. """ -import sys -import pytest from _pytest.pytester import Pytester +import pytest TESTCASES = [ @@ -23,10 +22,14 @@ TESTCASES = [ E assert [1, 4, 3] == [1, 2, 3] E At index 1 diff: 4 != 2 E Full diff: - E - [1, 2, 3] + E [ + E 1, + E - 2, E ? ^ - E + [1, 4, 3] + E + 4, E ? ^ + E 3, + E ] """, id="Compare lists, one item differs", ), @@ -42,9 +45,11 @@ TESTCASES = [ E assert [1, 2, 3] == [1, 2] E Left contains one more item: 3 E Full diff: - E - [1, 2] - E + [1, 2, 3] - E ? +++ + E [ + E 1, + E 2, + E + 3, + E ] """, id="Compare lists, one extra item", ), @@ -61,9 +66,11 @@ TESTCASES = [ E At index 1 diff: 3 != 2 E Right contains one more item: 3 E Full diff: - E - [1, 2, 3] - E ? --- - E + [1, 3] + E [ + E 1, + E - 2, + E 3, + E ] """, id="Compare lists, one item missing", ), @@ -79,10 +86,14 @@ TESTCASES = [ E assert (1, 4, 3) == (1, 2, 3) E At index 1 diff: 4 != 2 E Full diff: - E - (1, 2, 3) + E ( + E 1, + E - 2, E ? ^ - E + (1, 4, 3) + E + 4, E ? ^ + E 3, + E ) """, id="Compare tuples", ), @@ -101,10 +112,12 @@ TESTCASES = [ E Extra items in the right set: E 2 E Full diff: - E - {1, 2, 3} - E ? ^ ^ - E + {1, 3, 4} - E ? ^ ^ + E { + E 1, + E - 2, + E 3, + E + 4, + E } """, id="Compare sets", ), @@ -125,10 +138,13 @@ TESTCASES = [ E Right contains 1 more item: E {2: 'eggs'} E Full diff: - E - {1: 'spam', 2: 'eggs'} - E ? ^ - E + {1: 'spam', 3: 'eggs'} - E ? ^ + E { + E 1: 'spam', + E - 2: 'eggs', + E ? ^ + E + 3: 'eggs', + E ? ^ + E } """, id="Compare dicts with differing keys", ), @@ -147,10 +163,11 @@ TESTCASES = [ E Differing items: E {2: 'eggs'} != {2: 'bacon'} E Full diff: - E - {1: 'spam', 2: 'bacon'} - E ? ^^^^^ - E + {1: 'spam', 2: 'eggs'} - E ? ^^^^ + E { + E 1: 'spam', + E - 2: 'bacon', + E + 2: 'eggs', + E } """, id="Compare dicts with differing values", ), @@ -171,10 +188,11 @@ TESTCASES = [ E Right contains 1 more item: E {3: 'bacon'} E Full diff: - E - {1: 'spam', 3: 'bacon'} - E ? ^ ^^^^^ - E + {1: 'spam', 2: 'eggs'} - E ? ^ ^^^^ + E { + E 1: 'spam', + E - 3: 'bacon', + E + 2: 'eggs', + E } """, id="Compare dicts with differing items", ), @@ -210,68 +228,61 @@ TESTCASES = [ """, id='Test "not in" string', ), -] -if sys.version_info[:2] >= (3, 7): - TESTCASES.extend( - [ - pytest.param( - """ - from dataclasses import dataclass + pytest.param( + """ + from dataclasses import dataclass - @dataclass - class A: - a: int - b: str + @dataclass + class A: + a: int + b: str - def test_this(): - result = A(1, 'spam') - expected = A(2, 'spam') - assert result == expected - """, - """ - > assert result == expected - E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam') - E Matching attributes: - E ['b'] - E Differing attributes: - E ['a'] - E Drill down into differing attribute a: - E a: 1 != 2 - E +1 - E -2 - """, - id="Compare data classes", - ), - pytest.param( - """ - import attr + def test_this(): + result = A(1, 'spam') + expected = A(2, 'spam') + assert result == expected + """, + """ + > assert result == expected + E AssertionError: assert A(a=1, b='spam') == A(a=2, b='spam') + E Matching attributes: + E ['b'] + E Differing attributes: + E ['a'] + E Drill down into differing attribute a: + E a: 1 != 2 + """, + id="Compare data classes", + ), + pytest.param( + """ + import attr - @attr.s(auto_attribs=True) - class A: - a: int - b: str + @attr.s(auto_attribs=True) + class A: + a: int + b: str - def test_this(): - result = A(1, 'spam') - expected = A(1, 'eggs') - assert result == expected - """, - """ - > assert result == expected - E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs') - E Matching attributes: - E ['a'] - E Differing attributes: - E ['b'] - E Drill down into differing attribute b: - E b: 'spam' != 'eggs' - E - eggs - E + spam - """, - id="Compare attrs classes", - ), - ] - ) + def test_this(): + result = A(1, 'spam') + expected = A(1, 'eggs') + assert result == expected + """, + """ + > assert result == expected + E AssertionError: assert A(a=1, b='spam') == A(a=1, b='eggs') + E Matching attributes: + E ['a'] + E Differing attributes: + E ['b'] + E Drill down into differing attribute b: + E b: 'spam' != 'eggs' + E - eggs + E + spam + """, + id="Compare attrs classes", + ), +] @pytest.mark.parametrize("code, expected", TESTCASES) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_faulthandler.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_faulthandler.py index 5b7911f21f8..a3363de9816 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_faulthandler.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_faulthandler.py @@ -1,8 +1,9 @@ +# mypy: allow-untyped-defs import io import sys -import pytest from _pytest.pytester import Pytester +import pytest def test_enabled(pytester: Pytester) -> None: @@ -113,6 +114,7 @@ def test_cancel_timeout_on_hook(monkeypatch, hook_name) -> None: to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive exception (pytest-dev/pytest-faulthandler#14).""" import faulthandler + from _pytest import faulthandler as faulthandler_plugin called = [] diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_findpaths.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_findpaths.py index 3a2917261a2..260b9d07c9c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_findpaths.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_findpaths.py @@ -1,11 +1,14 @@ +# mypy: allow-untyped-defs +import os from pathlib import Path from textwrap import dedent -import pytest from _pytest.config import UsageError from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_dirs_from_args +from _pytest.config.findpaths import is_fs_root from _pytest.config.findpaths import load_config_dict_from_file +import pytest class TestLoadConfigDictFromFile: @@ -107,18 +110,19 @@ class TestCommonAncestor: fn2 = tmp_path / "foo" / "zaz" / "test_2.py" fn2.parent.mkdir(parents=True) fn2.touch() - assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo" - assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo" - assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo" - assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo" + cwd = Path.cwd() + assert get_common_ancestor(cwd, [fn1, fn2]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1.parent, fn2]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1.parent, fn2.parent]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1, fn2.parent]) == tmp_path / "foo" def test_single_dir(self, tmp_path: Path) -> None: - assert get_common_ancestor([tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [tmp_path]) == tmp_path def test_single_file(self, tmp_path: Path) -> None: fn = tmp_path / "foo.py" fn.touch() - assert get_common_ancestor([fn]) == tmp_path + assert get_common_ancestor(Path.cwd(), [fn]) == tmp_path def test_get_dirs_from_args(tmp_path): @@ -133,3 +137,18 @@ def test_get_dirs_from_args(tmp_path): assert get_dirs_from_args( [str(fn), str(tmp_path / "does_not_exist"), str(d), option, xdist_rsync_option] ) == [fn.parent, d] + + +@pytest.mark.parametrize( + "path, expected", + [ + pytest.param( + f"e:{os.sep}", True, marks=pytest.mark.skipif("sys.platform != 'win32'") + ), + (f"{os.sep}", True), + (f"e:{os.sep}projects", False), + (f"{os.sep}projects", False), + ], +) +def test_is_fs_root(path: Path, expected: bool) -> None: + assert is_fs_root(Path(path)) is expected diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_helpconfig.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_helpconfig.py index 44c2c9295bf..4906ef5c8f0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_helpconfig.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_helpconfig.py @@ -1,6 +1,7 @@ -import pytest +# mypy: allow-untyped-defs from _pytest.config import ExitCode from _pytest.pytester import Pytester +import pytest def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None: @@ -30,11 +31,11 @@ def test_help(pytester: Pytester) -> None: assert result.ret == 0 result.stdout.fnmatch_lines( """ - -m MARKEXPR only run tests matching given mark expression. - For example: -m 'mark1 and not mark2'. - reporting: + -m MARKEXPR Only run tests matching given mark expression. For + example: -m 'mark1 and not mark2'. + Reporting: --durations=N * - -V, --version display pytest version and information about plugins. + -V, --version Display pytest version and information about plugins. When given twice, also display information about plugins. *setup.cfg* @@ -71,9 +72,9 @@ def test_empty_help_param(pytester: Pytester) -> None: assert result.ret == 0 lines = [ " required_plugins (args):", - " plugins that must be present for pytest to run*", + " Plugins that must be present for pytest to run*", " test_ini (bool):*", - "environment variables:", + "Environment variables:", ] result.stdout.fnmatch_lines(lines, consecutive=True) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_junitxml.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_junitxml.py index 02531e81435..86edfbbeb61 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_junitxml.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_junitxml.py @@ -1,7 +1,8 @@ -import os -import platform +# mypy: allow-untyped-defs from datetime import datetime +import os from pathlib import Path +import platform from typing import cast from typing import List from typing import Optional @@ -12,7 +13,6 @@ from xml.dom import minidom import xmlschema -import pytest from _pytest.config import Config from _pytest.junitxml import bin_xml_escape from _pytest.junitxml import LogXML @@ -22,13 +22,14 @@ from _pytest.pytester import RunResult from _pytest.reports import BaseReport from _pytest.reports import TestReport from _pytest.stash import Stash +import pytest @pytest.fixture(scope="session") def schema() -> xmlschema.XMLSchema: """Return an xmlschema.XMLSchema object for the junit-10.xsd file.""" fn = Path(__file__).parent / "example_scripts/junit-10.xsd" - with fn.open() as f: + with fn.open(encoding="utf-8") as f: return xmlschema.XMLSchema(f) @@ -41,11 +42,11 @@ class RunAndParse: self, *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1" ) -> Tuple[RunResult, "DomNode"]: if family: - args = ("-o", "junit_family=" + family) + args + args = ("-o", "junit_family=" + family, *args) xml_path = self.pytester.path.joinpath("junit.xml") result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args) if family == "xunit2": - with xml_path.open() as f: + with xml_path.open(encoding="utf-8") as f: self.schema.validate(f) xmldoc = minidom.parse(str(xml_path)) return result, DomNode(xmldoc) @@ -253,7 +254,6 @@ class TestPython: duration_report: str, run_and_parse: RunAndParse, ) -> None: - # mock LogXML.node_reporter so it always sets a known duration to each test report object original_node_reporter = LogXML.node_reporter @@ -470,7 +470,7 @@ class TestPython: self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: p = pytester.mkdir("sub").joinpath("test_hello.py") - p.write_text("def test_func(): 0/0") + p.write_text("def test_func(): 0/0", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -603,7 +603,6 @@ class TestPython: node.assert_attr(failures=3, tests=3) for index, char in enumerate("<&'"): - tnode = node.find_nth_by_tag("testcase", index) tnode.assert_attr( classname="test_failure_escape", name="test_func[%s]" % char @@ -989,7 +988,7 @@ class TestNonPython: return "custom item runtest failed" """ ) - pytester.path.joinpath("myfile.xyz").write_text("hello") + pytester.path.joinpath("myfile.xyz").write_text("hello", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -1003,7 +1002,7 @@ class TestNonPython: @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) def test_nullbyte(pytester: Pytester, junit_logging: str) -> None: - # A null byte can not occur in XML (see section 2.2 of the spec) + # A null byte cannot occur in XML (see section 2.2 of the spec) pytester.makepyfile( """ import sys @@ -1015,7 +1014,7 @@ def test_nullbyte(pytester: Pytester, junit_logging: str) -> None: ) xmlf = pytester.path.joinpath("junit.xml") pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read_text() + text = xmlf.read_text(encoding="utf-8") assert "\x00" not in text if junit_logging == "system-out": assert "#x00" in text @@ -1037,7 +1036,7 @@ def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None: ) xmlf = pytester.path.joinpath("junit.xml") pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read_text() + text = xmlf.read_text(encoding="utf-8") if junit_logging == "system-out": assert "#x0" in text if junit_logging == "no": @@ -1203,7 +1202,7 @@ def test_unicode_issue368(pytester: Pytester) -> None: node_reporter.append_skipped(test_report) test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣" node_reporter.append_skipped(test_report) - test_report.wasxfail = ustr # type: ignore[attr-defined] + test_report.wasxfail = ustr node_reporter.append_skipped(test_report) log.pytest_sessionfinish() @@ -1230,6 +1229,36 @@ def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None result.stdout.fnmatch_lines(["*= 1 passed in *"]) +def test_record_property_on_test_and_teardown_failure( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def other(record_property): + record_property("bar", 1) + yield + assert 0 + + def test_record(record_property, other): + record_property("foo", "<1") + assert 0 + """ + ) + result, dom = run_and_parse() + node = dom.find_first_by_tag("testsuite") + tnodes = node.find_by_tag("testcase") + for tnode in tnodes: + psnode = tnode.find_first_by_tag("properties") + assert psnode, f"testcase didn't had expected properties:\n{tnode}" + pnodes = psnode.find_by_tag("property") + pnodes[0].assert_attr(name="bar", value="1") + pnodes[1].assert_attr(name="foo", value="<1") + result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"]) + + def test_record_property_same_name( pytester: Pytester, run_and_parse: RunAndParse ) -> None: @@ -1254,12 +1283,10 @@ def test_record_fixtures_without_junitxml( pytester: Pytester, fixture_name: str ) -> None: pytester.makepyfile( - """ + f""" def test_record({fixture_name}): {fixture_name}("foo", "bar") - """.format( - fixture_name=fixture_name - ) + """ ) result = pytester.runpytest() assert result.ret == 0 @@ -1307,7 +1334,7 @@ def test_record_fixtures_xunit2( """ ) pytester.makepyfile( - """ + f""" import pytest @pytest.fixture @@ -1315,9 +1342,7 @@ def test_record_fixtures_xunit2( {fixture_name}("bar", 1) def test_record({fixture_name}, other): {fixture_name}("foo", "<1"); - """.format( - fixture_name=fixture_name - ) + """ ) result, dom = run_and_parse(family=None) @@ -1327,10 +1352,8 @@ def test_record_fixtures_xunit2( "*test_record_fixtures_xunit2.py:6:*record_xml_attribute is an experimental feature" ) expected_lines = [ - "*test_record_fixtures_xunit2.py:6:*{fixture_name} is incompatible " - "with junit_family 'xunit2' (use 'legacy' or 'xunit1')".format( - fixture_name=fixture_name - ) + f"*test_record_fixtures_xunit2.py:6:*{fixture_name} is incompatible " + "with junit_family 'xunit2' (use 'legacy' or 'xunit1')" ] result.stdout.fnmatch_lines(expected_lines) @@ -1447,7 +1470,12 @@ def test_fancy_items_regression(pytester: Pytester, run_and_parse: RunAndParse) result.stdout.no_fnmatch_line("*INTERNALERROR*") - items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase")) + items = sorted( + "%(classname)s %(name)s" % x # noqa: UP031 + # dom is a DomNode not a mapping, it's not possible to ** it. + for x in dom.find_by_tag("testcase") + ) + import pprint pprint.pprint(items) @@ -1582,13 +1610,11 @@ def test_set_suite_name( ) -> None: if suite_name: pytester.makeini( - """ + f""" [pytest] junit_suite_name={suite_name} - junit_family={family} - """.format( - suite_name=suite_name, family=xunit_family - ) + junit_family={xunit_family} + """ ) expected = suite_name else: @@ -1625,19 +1651,56 @@ def test_escaped_skipreason_issue3533( snode.assert_attr(message="1 <> 2") +def test_bin_escaped_skipreason(pytester: Pytester, run_and_parse: RunAndParse) -> None: + """Escape special characters from mark.skip reason (#11842).""" + pytester.makepyfile( + """ + import pytest + @pytest.mark.skip("\33[31;1mred\33[0m") + def test_skip(): + pass + """ + ) + _, dom = run_and_parse() + node = dom.find_first_by_tag("testcase") + snode = node.find_first_by_tag("skipped") + assert "#x1B[31;1mred#x1B[0m" in snode.text + snode.assert_attr(message="#x1B[31;1mred#x1B[0m") + + +def test_escaped_setup_teardown_error( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.fixture() + def my_setup(): + raise Exception("error: \033[31mred\033[m") + + def test_esc(my_setup): + pass + """ + ) + _, dom = run_and_parse() + node = dom.find_first_by_tag("testcase") + snode = node.find_first_by_tag("error") + assert "#x1B[31mred#x1B[m" in snode["message"] + assert "#x1B[31mred#x1B[m" in snode.text + + @parametrize_families def test_logging_passing_tests_disabled_does_not_log_test_output( pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: pytester.makeini( - """ + f""" [pytest] junit_log_passing_tests=False junit_logging=system-out - junit_family={family} - """.format( - family=xunit_family - ) + junit_family={xunit_family} + """ ) pytester.makepyfile( """ @@ -1667,13 +1730,11 @@ def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( xunit_family: str, ) -> None: pytester.makeini( - """ + f""" [pytest] junit_log_passing_tests=False - junit_family={family} - """.format( - family=xunit_family - ) + junit_family={xunit_family} + """ ) pytester.makepyfile( """ diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_legacypath.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_legacypath.py index 8acafe98e7c..ad4e22e46b4 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_legacypath.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_legacypath.py @@ -1,9 +1,11 @@ +# mypy: allow-untyped-defs from pathlib import Path -import pytest from _pytest.compat import LEGACY_PATH +from _pytest.fixtures import TopRequest from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir +import pytest def test_item_fspath(pytester: pytest.Pytester) -> None: @@ -14,7 +16,7 @@ def test_item_fspath(pytester: pytest.Pytester) -> None: items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name - assert item2.fspath == item.fspath # type: ignore[attr-defined] + assert item2.fspath == item.fspath assert item2.path == item.path @@ -90,7 +92,8 @@ def test_cache_makedir(cache: pytest.Cache) -> None: def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None: modcol = pytester.getmodulecol("def test_somefunc(): pass") (item,) = pytester.genitems([modcol]) - req = pytest.FixtureRequest(item, _ispytest=True) + assert isinstance(item, pytest.Function) + req = TopRequest(item, _ispytest=True) assert req.path == modcol.path assert req.fspath == modcol.fspath # type: ignore[attr-defined] @@ -105,7 +108,7 @@ class TestFixtureRequestSessionScoped: AttributeError, match="path not available in session-scoped context", ): - session_request.fspath + _ = session_request.fspath @pytest.mark.parametrize("config_type", ["ini", "pyproject"]) @@ -152,7 +155,7 @@ def test_override_ini_paths(pytester: pytest.Pytester) -> None: ) pytester.makepyfile( r""" - def test_overriden(pytestconfig): + def test_overridden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_link_resolve.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_link_resolve.py index 60a86ada36e..0461cd75554 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_link_resolve.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_link_resolve.py @@ -1,10 +1,11 @@ +# mypy: allow-untyped-defs +from contextlib import contextmanager import os.path +from pathlib import Path +from string import ascii_lowercase import subprocess import sys import textwrap -from contextlib import contextmanager -from pathlib import Path -from string import ascii_lowercase from _pytest.pytester import Pytester @@ -59,7 +60,8 @@ def test_link_resolve(pytester: Pytester) -> None: def test_foo(): raise AssertionError() """ - ) + ), + encoding="utf-8", ) subst = subst_path_linux diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_main.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_main.py index 2df51bb7bb9..6294f66b360 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_main.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_main.py @@ -1,16 +1,17 @@ +# mypy: allow-untyped-defs import argparse import os -import re -import sys from pathlib import Path +import re from typing import Optional -import pytest from _pytest.config import ExitCode from _pytest.config import UsageError +from _pytest.main import CollectionArgument from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp from _pytest.pytester import Pytester +import pytest @pytest.mark.parametrize( @@ -24,19 +25,17 @@ from _pytest.pytester import Pytester def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None: returncode, exc = ret_exc c1 = pytester.makeconftest( - """ + f""" import pytest def pytest_sessionstart(): - raise {exc}("boom") + raise {exc.__name__}("boom") def pytest_internalerror(excrepr, excinfo): returncode = {returncode!r} if returncode is not False: pytest.exit("exiting after %s..." % excinfo.typename, returncode={returncode!r}) - """.format( - returncode=returncode, exc=exc.__name__ - ) + """ ) result = pytester.runpytest() if returncode: @@ -45,32 +44,18 @@ def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None: assert result.ret == ExitCode.INTERNAL_ERROR assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):" - end_lines = ( - result.stdout.lines[-4:] - if sys.version_info >= (3, 11) - else result.stdout.lines[-3:] - ) + end_lines = result.stdout.lines[-3:] if exc == SystemExit: assert end_lines == [ f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise SystemExit("boom")', - *( - ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if sys.version_info >= (3, 11) - else () - ), "INTERNALERROR> SystemExit: boom", ] else: assert end_lines == [ f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise ValueError("boom")', - *( - ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if sys.version_info >= (3, 11) - else () - ), "INTERNALERROR> ValueError: boom", ] if returncode is False: @@ -84,13 +69,11 @@ def test_wrap_session_exit_sessionfinish( returncode: Optional[int], pytester: Pytester ) -> None: pytester.makeconftest( - """ + f""" import pytest def pytest_sessionfinish(): pytest.exit(reason="exit_pytest_sessionfinish", returncode={returncode}) - """.format( - returncode=returncode - ) + """ ) result = pytester.runpytest() if returncode: @@ -136,26 +119,43 @@ class TestResolveCollectionArgument: def test_file(self, invocation_path: Path) -> None: """File and parts.""" - assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( - invocation_path / "src/pkg/test.py", - [], + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py" + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=[], + module_name=None, ) - assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( - invocation_path / "src/pkg/test.py", - [""], + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py::" + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=[""], + module_name=None, ) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar" - ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=["foo", "bar"], + module_name=None, + ) assert resolve_collection_argument( invocation_path, "src/pkg/test.py::foo::bar::" - ) == (invocation_path / "src/pkg/test.py", ["foo", "bar", ""]) + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=["foo", "bar", ""], + module_name=None, + ) def test_dir(self, invocation_path: Path) -> None: """Directory and parts.""" - assert resolve_collection_argument(invocation_path, "src/pkg") == ( - invocation_path / "src/pkg", - [], + assert resolve_collection_argument( + invocation_path, "src/pkg" + ) == CollectionArgument( + path=invocation_path / "src/pkg", + parts=[], + module_name=None, ) with pytest.raises( @@ -172,13 +172,24 @@ class TestResolveCollectionArgument: """Dotted name and parts.""" assert resolve_collection_argument( invocation_path, "pkg.test", as_pypath=True - ) == (invocation_path / "src/pkg/test.py", []) + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=[], + module_name="pkg.test", + ) assert resolve_collection_argument( invocation_path, "pkg.test::foo::bar", as_pypath=True - ) == (invocation_path / "src/pkg/test.py", ["foo", "bar"]) - assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == ( - invocation_path / "src/pkg", - [], + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=["foo", "bar"], + module_name="pkg.test", + ) + assert resolve_collection_argument( + invocation_path, "pkg", as_pypath=True + ) == CollectionArgument( + path=invocation_path / "src/pkg", + parts=[], + module_name="pkg", ) with pytest.raises( @@ -189,10 +200,13 @@ class TestResolveCollectionArgument: ) def test_parametrized_name_with_colons(self, invocation_path: Path) -> None: - ret = resolve_collection_argument( + assert resolve_collection_argument( invocation_path, "src/pkg/test.py::test[a::b]" + ) == CollectionArgument( + path=invocation_path / "src/pkg/test.py", + parts=["test[a::b]"], + module_name=None, ) - assert ret == (invocation_path / "src/pkg/test.py", ["test[a::b]"]) def test_does_not_exist(self, invocation_path: Path) -> None: """Given a file/module that does not exist raises UsageError.""" @@ -212,9 +226,12 @@ class TestResolveCollectionArgument: def test_absolute_paths_are_resolved_correctly(self, invocation_path: Path) -> None: """Absolute paths resolve back to absolute paths.""" full_path = str(invocation_path / "src") - assert resolve_collection_argument(invocation_path, full_path) == ( - Path(os.path.abspath("src")), - [], + assert resolve_collection_argument( + invocation_path, full_path + ) == CollectionArgument( + path=Path(os.path.abspath("src")), + parts=[], + module_name=None, ) # ensure full paths given in the command-line without the drive letter resolve @@ -222,7 +239,11 @@ class TestResolveCollectionArgument: drive, full_path_without_drive = os.path.splitdrive(full_path) assert resolve_collection_argument( invocation_path, full_path_without_drive - ) == (Path(os.path.abspath("src")), []) + ) == CollectionArgument( + path=Path(os.path.abspath("src")), + parts=[], + module_name=None, + ) def test_module_full_path_without_drive(pytester: Pytester) -> None: @@ -262,3 +283,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None: "* 1 passed in *", ] ) + + +def test_very_long_cmdline_arg(pytester: Pytester) -> None: + """ + Regression test for #11394. + + Note: we could not manage to actually reproduce the error with this code, we suspect + GitHub runners are configured to support very long paths, however decided to leave + the test in place in case this ever regresses in the future. + """ + pytester.makeconftest( + """ + import pytest + + def pytest_addoption(parser): + parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things") + + @pytest.fixture(scope="module") + def specified_feeds(request): + list_string = request.config.getoption("--long-list") + return list_string.split(',') + """ + ) + pytester.makepyfile( + """ + def test_foo(specified_feeds): + assert len(specified_feeds) == 100_000 + """ + ) + result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000)) + result.stdout.fnmatch_lines("* 1 passed *") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_mark.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_mark.py index da67d1ea7bc..2896afa4532 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_mark.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_mark.py @@ -1,23 +1,24 @@ +# mypy: allow-untyped-defs import os import sys from typing import List from typing import Optional from unittest import mock -import pytest from _pytest.config import ExitCode from _pytest.mark import MarkGenerator from _pytest.mark.structures import EMPTY_PARAMETERSET_OPTION from _pytest.nodes import Collector from _pytest.nodes import Node from _pytest.pytester import Pytester +import pytest class TestMark: @pytest.mark.parametrize("attr", ["mark", "param"]) def test_pytest_exists_in_namespace_all(self, attr: str) -> None: module = sys.modules["pytest"] - assert attr in module.__all__ # type: ignore + assert attr in module.__all__ def test_pytest_mark_notcallable(self) -> None: mark = MarkGenerator(_ispytest=True) @@ -33,7 +34,7 @@ class TestMark: assert pytest.mark.foo(some_function) is some_function marked_with_args = pytest.mark.foo.with_args(some_function) - assert marked_with_args is not some_function # type: ignore[comparison-overlap] + assert marked_with_args is not some_function assert pytest.mark.foo(SomeClass) is SomeClass assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] @@ -41,7 +42,7 @@ class TestMark: def test_pytest_mark_name_starts_with_underscore(self) -> None: mark = MarkGenerator(_ispytest=True) with pytest.raises(AttributeError): - mark._some_name + _ = mark._some_name def test_marked_class_run_twice(pytester: Pytester) -> None: @@ -806,12 +807,12 @@ class TestKeywordSelection: pytester.makepyfile( conftest=""" import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(name): - outcome = yield + item = yield if name == "TestClass": - item = outcome.get_result() item.extra_keyword_matches.add("xxx") + return item """ ) reprec = pytester.inline_run(p.parent, "-s", "-k", keyword) @@ -823,25 +824,6 @@ class TestKeywordSelection: assert len(dlist) == 1 assert dlist[0].items[0].name == "test_1" - def test_select_starton(self, pytester: Pytester) -> None: - threepass = pytester.makepyfile( - test_threepass=""" - def test_one(): assert 1 - def test_two(): assert 1 - def test_three(): assert 1 - """ - ) - reprec = pytester.inline_run( - "-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", threepass - ) - passed, skipped, failed = reprec.listoutcomes() - assert len(passed) == 2 - assert not failed - dlist = reprec.getcalls("pytest_deselected") - assert len(dlist) == 1 - item = dlist[0].items[0] - assert item.name == "test_one" - def test_keyword_extra(self, pytester: Pytester) -> None: p = pytester.makepyfile( """ @@ -890,17 +872,30 @@ class TestKeywordSelection: deselected_tests = dlist[0].items assert len(deselected_tests) == 1 - def test_no_match_directories_outside_the_suite(self, pytester: Pytester) -> None: + def test_no_match_directories_outside_the_suite( + self, + pytester: Pytester, + monkeypatch: pytest.MonkeyPatch, + ) -> None: """`-k` should not match against directories containing the test suite (#7040).""" - test_contents = """ - def test_aaa(): pass - def test_ddd(): pass - """ + pytester.makefile( + **{ + "suite/pytest": """[pytest]""", + }, + ext=".ini", + ) pytester.makepyfile( - **{"ddd/tests/__init__.py": "", "ddd/tests/test_foo.py": test_contents} + **{ + "suite/ddd/tests/__init__.py": "", + "suite/ddd/tests/test_foo.py": """ + def test_aaa(): pass + def test_ddd(): pass + """, + } ) + monkeypatch.chdir(pytester.path / "suite") - def get_collected_names(*args): + def get_collected_names(*args: str) -> List[str]: _, rec = pytester.inline_genitems(*args) calls = rec.getcalls("pytest_collection_finish") assert len(calls) == 1 @@ -912,12 +907,6 @@ class TestKeywordSelection: # do not collect anything based on names outside the collection tree assert get_collected_names("-k", pytester._name) == [] - # "-k ddd" should only collect "test_ddd", but not - # 'test_aaa' just because one of its parent directories is named "ddd"; - # this was matched previously because Package.name would contain the full path - # to the package - assert get_collected_names("-k", "ddd") == ["test_ddd"] - class TestMarkDecorator: @pytest.mark.parametrize( @@ -945,16 +934,15 @@ def test_parameterset_for_parametrize_marks( ) -> None: if mark is not None: pytester.makeini( - """ + f""" [pytest] - {}={} - """.format( - EMPTY_PARAMETERSET_OPTION, mark - ) + {EMPTY_PARAMETERSET_OPTION}={mark} + """ ) config = pytester.parseconfig() - from _pytest.mark import pytest_configure, get_empty_parameterset_mark + from _pytest.mark import get_empty_parameterset_mark + from _pytest.mark import pytest_configure pytest_configure(config) result_mark = get_empty_parameterset_mark(config, ["a"], all) @@ -969,16 +957,15 @@ def test_parameterset_for_parametrize_marks( def test_parameterset_for_fail_at_collect(pytester: Pytester) -> None: pytester.makeini( - """ + f""" [pytest] - {}=fail_at_collect - """.format( - EMPTY_PARAMETERSET_OPTION - ) + {EMPTY_PARAMETERSET_OPTION}=fail_at_collect + """ ) config = pytester.parseconfig() - from _pytest.mark import pytest_configure, get_empty_parameterset_mark + from _pytest.mark import get_empty_parameterset_mark + from _pytest.mark import pytest_configure pytest_configure(config) @@ -1128,3 +1115,62 @@ def test_marker_expr_eval_failure_handling(pytester: Pytester, expr) -> None: result = pytester.runpytest(foo, "-m", expr) result.stderr.fnmatch_lines([expected]) assert result.ret == ExitCode.USAGE_ERROR + + +def test_mark_mro() -> None: + xfail = pytest.mark.xfail + + @xfail("a") + class A: + pass + + @xfail("b") + class B: + pass + + @xfail("c") + class C(A, B): + pass + + from _pytest.mark.structures import get_unpacked_marks + + all_marks = get_unpacked_marks(C) + + assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark] + + assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark] + + +# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447") +def test_mark_fixture_order_mro(pytester: Pytester): + """This ensures we walk marks of the mro starting with the base classes + the action at a distance fixtures are taken as minimal example from a real project + + """ + foo = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def add_attr1(request): + request.instance.attr1 = object() + + + @pytest.fixture + def add_attr2(request): + request.instance.attr2 = request.instance.attr1 + + + @pytest.mark.usefixtures('add_attr1') + class Parent: + pass + + + @pytest.mark.usefixtures('add_attr2') + class TestThings(Parent): + def test_attrs(self): + assert self.attr1 == self.attr2 + """ + ) + result = pytester.runpytest(foo) + result.assert_outcomes(passed=1) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_mark_expression.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_mark_expression.py index f3643e7b409..07c89f90838 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_mark_expression.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_mark_expression.py @@ -1,8 +1,8 @@ from typing import Callable -import pytest from _pytest.mark.expression import Expression from _pytest.mark.expression import ParseError +import pytest def evaluate(input: str, matcher: Callable[[str], bool]) -> bool: @@ -61,7 +61,7 @@ def test_basic(expr: str, expected: bool) -> None: ("not not not not not true", False), ), ) -def test_syntax_oddeties(expr: str, expected: bool) -> None: +def test_syntax_oddities(expr: str, expected: bool) -> None: matcher = {"true": True, "false": False}.__getitem__ assert evaluate(expr, matcher) is expected diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_meta.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_meta.py index 9201bd21611..40ed95d6b47 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_meta.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_meta.py @@ -3,6 +3,7 @@ This ensures all internal packages can be imported without needing the pytest namespace being set, which is critical for the initialization of xdist. """ + import pkgutil import subprocess import sys diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_monkeypatch.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_monkeypatch.py index 95521818021..12be774beca 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_monkeypatch.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_monkeypatch.py @@ -1,15 +1,16 @@ +# mypy: allow-untyped-defs import os +from pathlib import Path import re import sys import textwrap -from pathlib import Path from typing import Dict from typing import Generator from typing import Type -import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest @pytest.fixture @@ -50,21 +51,24 @@ def test_setattr() -> None: class TestSetattrWithImportPath: def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr("os.path.abspath", lambda x: "hello2") - assert os.path.abspath("123") == "hello2" + with monkeypatch.context() as mp: + mp.setattr("os.path.abspath", lambda x: "hello2") + assert os.path.abspath("123") == "hello2" def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr("_pytest.config.Config", 42) - import _pytest + with monkeypatch.context() as mp: + mp.setattr("_pytest.config.Config", 42) + import _pytest - assert _pytest.config.Config == 42 # type: ignore + assert _pytest.config.Config == 42 # type: ignore def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: - monkeypatch.setattr("_pytest.config.Config", 42) - import _pytest + with monkeypatch.context() as mp: + mp.setattr("_pytest.config.Config", 42) + import _pytest - assert _pytest.config.Config == 42 # type: ignore - monkeypatch.delattr("_pytest.config.Config") + assert _pytest.config.Config == 42 # type: ignore + mp.delattr("_pytest.config.Config") def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: with pytest.raises(TypeError): @@ -80,14 +84,16 @@ class TestSetattrWithImportPath: def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: # https://github.com/pytest-dev/pytest/issues/746 - monkeypatch.setattr("os.path.qweqwe", 42, raising=False) - assert os.path.qweqwe == 42 # type: ignore + with monkeypatch.context() as mp: + mp.setattr("os.path.qweqwe", 42, raising=False) + assert os.path.qweqwe == 42 # type: ignore def test_delattr(self, monkeypatch: MonkeyPatch) -> None: - monkeypatch.delattr("os.path.abspath") - assert not hasattr(os.path, "abspath") - monkeypatch.undo() - assert os.path.abspath + with monkeypatch.context() as mp: + mp.delattr("os.path.abspath") + assert not hasattr(os.path, "abspath") + mp.undo() + assert os.path.abspath # type:ignore[truthy-function] def test_delattr() -> None: @@ -319,7 +325,8 @@ def test_importerror(pytester: Pytester) -> None: x = 1 """ - ) + ), + encoding="utf-8", ) pytester.path.joinpath("test_importerror.py").write_text( textwrap.dedent( @@ -327,7 +334,8 @@ def test_importerror(pytester: Pytester) -> None: def test_importerror(monkeypatch): monkeypatch.setattr('package.a.x', 2) """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines( @@ -420,6 +428,7 @@ def test_context_classmethod() -> None: assert A.x == 1 +@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning") def test_syspath_prepend_with_namespace_packages( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: @@ -428,11 +437,13 @@ def test_syspath_prepend_with_namespace_packages( ns = d.joinpath("ns_pkg") ns.mkdir() ns.joinpath("__init__.py").write_text( - "__import__('pkg_resources').declare_namespace(__name__)" + "__import__('pkg_resources').declare_namespace(__name__)", encoding="utf-8" ) lib = ns.joinpath(dirname) lib.mkdir() - lib.joinpath("__init__.py").write_text("def check(): return %r" % dirname) + lib.joinpath("__init__.py").write_text( + "def check(): return %r" % dirname, encoding="utf-8" + ) monkeypatch.syspath_prepend("hello") import ns_pkg.hello @@ -451,5 +462,5 @@ def test_syspath_prepend_with_namespace_packages( # Should invalidate caches via importlib.invalidate_caches. modules_tmpdir = pytester.mkdir("modules_tmpdir") monkeypatch.syspath_prepend(str(modules_tmpdir)) - modules_tmpdir.joinpath("main_app.py").write_text("app = True") + modules_tmpdir.joinpath("main_app.py").write_text("app = True", encoding="utf-8") from main_app import app # noqa: F401 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_nodes.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_nodes.py index df1439e1c49..a3caf471f70 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_nodes.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_nodes.py @@ -1,39 +1,16 @@ -import re -import warnings +# mypy: allow-untyped-defs from pathlib import Path +import re from typing import cast -from typing import List from typing import Type +import warnings -import pytest from _pytest import nodes from _pytest.compat import legacy_path from _pytest.outcomes import OutcomeException from _pytest.pytester import Pytester from _pytest.warning_types import PytestWarning - - -@pytest.mark.parametrize( - ("nodeid", "expected"), - ( - ("", [""]), - ("a", ["", "a"]), - ("aa/b", ["", "aa", "aa/b"]), - ("a/b/c", ["", "a", "a/b", "a/b/c"]), - ("a/bbb/c::D", ["", "a", "a/bbb", "a/bbb/c", "a/bbb/c::D"]), - ("a/b/c::D::eee", ["", "a", "a/b", "a/b/c", "a/b/c::D", "a/b/c::D::eee"]), - ("::xx", ["", "::xx"]), - # / only considered until first :: - ("a/b/c::D/d::e", ["", "a", "a/b", "a/b/c", "a/b/c::D/d", "a/b/c::D/d::e"]), - # : alone is not a separator. - ("a/b::D:e:f::g", ["", "a", "a/b", "a/b::D:e:f", "a/b::D:e:f::g"]), - # / not considered if a part of a test name - ("a/b::c/d::e[/test]", ["", "a", "a/b", "a/b::c/d", "a/b::c/d::e[/test]"]), - ), -) -def test_iterparentnodeids(nodeid: str, expected: List[str]) -> None: - result = list(nodes.iterparentnodeids(nodeid)) - assert result == expected +import pytest def test_node_from_parent_disallowed_arguments() -> None: @@ -63,7 +40,6 @@ def test_subclassing_both_item_and_collector_deprecated( Verifies we warn on diamond inheritance as well as correctly managing legacy inheritance constructors with missing args as found in plugins. """ - # We do not expect any warnings messages to issued during class definition. with warnings.catch_warnings(): warnings.simplefilter("error") @@ -73,6 +49,12 @@ def test_subclassing_both_item_and_collector_deprecated( """Legacy ctor with legacy call # don't wana see""" super().__init__(fspath, parent) + def collect(self): + raise NotImplementedError() + + def runtest(self): + raise NotImplementedError() + with pytest.warns(PytestWarning) as rec: SoWrong.from_parent( request.session, fspath=legacy_path(tmp_path / "broken.txt") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_nose.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_nose.py deleted file mode 100644 index 1ded8854bb7..00000000000 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_nose.py +++ /dev/null @@ -1,498 +0,0 @@ -import pytest -from _pytest.pytester import Pytester - - -def setup_module(mod): - mod.nose = pytest.importorskip("nose") - - -def test_nose_setup(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - values = [] - from nose.tools import with_setup - - @with_setup(lambda: values.append(1), lambda: values.append(2)) - def test_hello(): - assert values == [1] - - def test_world(): - assert values == [1,2] - - test_hello.setup = lambda: values.append(1) - test_hello.teardown = lambda: values.append(2) - """ - ) - result = pytester.runpytest(p, "-p", "nose") - result.assert_outcomes(passed=2) - - -def test_setup_func_with_setup_decorator() -> None: - from _pytest.nose import call_optional - - values = [] - - class A: - @pytest.fixture(autouse=True) - def f(self): - values.append(1) - - call_optional(A(), "f") - assert not values - - -def test_setup_func_not_callable() -> None: - from _pytest.nose import call_optional - - class A: - f = 1 - - call_optional(A(), "f") - - -def test_nose_setup_func(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - from nose.tools import with_setup - - values = [] - - def my_setup(): - a = 1 - values.append(a) - - def my_teardown(): - b = 2 - values.append(b) - - @with_setup(my_setup, my_teardown) - def test_hello(): - print(values) - assert values == [1] - - def test_world(): - print(values) - assert values == [1,2] - - """ - ) - result = pytester.runpytest(p, "-p", "nose") - result.assert_outcomes(passed=2) - - -def test_nose_setup_func_failure(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - from nose.tools import with_setup - - values = [] - my_setup = lambda x: 1 - my_teardown = lambda x: 2 - - @with_setup(my_setup, my_teardown) - def test_hello(): - print(values) - assert values == [1] - - def test_world(): - print(values) - assert values == [1,2] - - """ - ) - result = pytester.runpytest(p, "-p", "nose") - result.stdout.fnmatch_lines(["*TypeError: <lambda>()*"]) - - -def test_nose_setup_func_failure_2(pytester: Pytester) -> None: - pytester.makepyfile( - """ - values = [] - - my_setup = 1 - my_teardown = 2 - - def test_hello(): - assert values == [] - - test_hello.setup = my_setup - test_hello.teardown = my_teardown - """ - ) - reprec = pytester.inline_run() - reprec.assertoutcome(passed=1) - - -def test_nose_setup_partial(pytester: Pytester) -> None: - pytest.importorskip("functools") - p = pytester.makepyfile( - """ - from functools import partial - - values = [] - - def my_setup(x): - a = x - values.append(a) - - def my_teardown(x): - b = x - values.append(b) - - my_setup_partial = partial(my_setup, 1) - my_teardown_partial = partial(my_teardown, 2) - - def test_hello(): - print(values) - assert values == [1] - - def test_world(): - print(values) - assert values == [1,2] - - test_hello.setup = my_setup_partial - test_hello.teardown = my_teardown_partial - """ - ) - result = pytester.runpytest(p, "-p", "nose") - result.stdout.fnmatch_lines(["*2 passed*"]) - - -def test_module_level_setup(pytester: Pytester) -> None: - pytester.makepyfile( - """ - from nose.tools import with_setup - items = {} - - def setup(): - items.setdefault("setup", []).append("up") - - def teardown(): - items.setdefault("setup", []).append("down") - - def setup2(): - items.setdefault("setup2", []).append("up") - - def teardown2(): - items.setdefault("setup2", []).append("down") - - def test_setup_module_setup(): - assert items["setup"] == ["up"] - - def test_setup_module_setup_again(): - assert items["setup"] == ["up"] - - @with_setup(setup2, teardown2) - def test_local_setup(): - assert items["setup"] == ["up"] - assert items["setup2"] == ["up"] - - @with_setup(setup2, teardown2) - def test_local_setup_again(): - assert items["setup"] == ["up"] - assert items["setup2"] == ["up", "down", "up"] - """ - ) - result = pytester.runpytest("-p", "nose") - result.stdout.fnmatch_lines(["*4 passed*"]) - - -def test_nose_style_setup_teardown(pytester: Pytester) -> None: - pytester.makepyfile( - """ - values = [] - - def setup_module(): - values.append(1) - - def teardown_module(): - del values[0] - - def test_hello(): - assert values == [1] - - def test_world(): - assert values == [1] - """ - ) - result = pytester.runpytest("-p", "nose") - result.stdout.fnmatch_lines(["*2 passed*"]) - - -def test_fixtures_nose_setup_issue8394(pytester: Pytester) -> None: - pytester.makepyfile( - """ - def setup_module(): - pass - - def teardown_module(): - pass - - def setup_function(func): - pass - - def teardown_function(func): - pass - - def test_world(): - pass - - class Test(object): - def setup_class(cls): - pass - - def teardown_class(cls): - pass - - def setup_method(self, meth): - pass - - def teardown_method(self, meth): - pass - - def test_method(self): pass - """ - ) - match = "*no docstring available*" - result = pytester.runpytest("--fixtures") - assert result.ret == 0 - result.stdout.no_fnmatch_line(match) - - result = pytester.runpytest("--fixtures", "-v") - assert result.ret == 0 - result.stdout.fnmatch_lines([match, match, match, match]) - - -def test_nose_setup_ordering(pytester: Pytester) -> None: - pytester.makepyfile( - """ - def setup_module(mod): - mod.visited = True - - class TestClass(object): - def setup(self): - assert visited - self.visited_cls = True - def test_first(self): - assert visited - assert self.visited_cls - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines(["*1 passed*"]) - - -def test_apiwrapper_problem_issue260(pytester: Pytester) -> None: - # this would end up trying a call an optional teardown on the class - # for plain unittests we don't want nose behaviour - pytester.makepyfile( - """ - import unittest - class TestCase(unittest.TestCase): - def setup(self): - #should not be called in unittest testcases - assert 0, 'setup' - def teardown(self): - #should not be called in unittest testcases - assert 0, 'teardown' - def setUp(self): - print('setup') - def tearDown(self): - print('teardown') - def test_fun(self): - pass - """ - ) - result = pytester.runpytest() - result.assert_outcomes(passed=1) - - -def test_setup_teardown_linking_issue265(pytester: Pytester) -> None: - # we accidentally didn't integrate nose setupstate with normal setupstate - # this test ensures that won't happen again - pytester.makepyfile( - ''' - import pytest - - class TestGeneric(object): - def test_nothing(self): - """Tests the API of the implementation (for generic and specialized).""" - - @pytest.mark.skipif("True", reason= - "Skip tests to check if teardown is skipped as well.") - class TestSkipTeardown(TestGeneric): - - def setup(self): - """Sets up my specialized implementation for $COOL_PLATFORM.""" - raise Exception("should not call setup for skipped tests") - - def teardown(self): - """Undoes the setup.""" - raise Exception("should not call teardown for skipped tests") - ''' - ) - reprec = pytester.runpytest() - reprec.assert_outcomes(passed=1, skipped=1) - - -def test_SkipTest_during_collection(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import nose - raise nose.SkipTest("during collection") - def test_failing(): - assert False - """ - ) - result = pytester.runpytest(p) - result.assert_outcomes(skipped=1, warnings=1) - - -def test_SkipTest_in_test(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import nose - - def test_skipping(): - raise nose.SkipTest("in test") - """ - ) - reprec = pytester.inline_run() - reprec.assertoutcome(skipped=1) - - -def test_istest_function_decorator(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import nose.tools - @nose.tools.istest - def not_test_prefix(): - pass - """ - ) - result = pytester.runpytest(p) - result.assert_outcomes(passed=1) - - -def test_nottest_function_decorator(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import nose.tools - @nose.tools.nottest - def test_prefix(): - pass - """ - ) - reprec = pytester.inline_run() - assert not reprec.getfailedcollections() - calls = reprec.getreports("pytest_runtest_logreport") - assert not calls - - -def test_istest_class_decorator(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import nose.tools - @nose.tools.istest - class NotTestPrefix(object): - def test_method(self): - pass - """ - ) - result = pytester.runpytest(p) - result.assert_outcomes(passed=1) - - -def test_nottest_class_decorator(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import nose.tools - @nose.tools.nottest - class TestPrefix(object): - def test_method(self): - pass - """ - ) - reprec = pytester.inline_run() - assert not reprec.getfailedcollections() - calls = reprec.getreports("pytest_runtest_logreport") - assert not calls - - -def test_skip_test_with_unicode(pytester: Pytester) -> None: - pytester.makepyfile( - """\ - import unittest - class TestClass(): - def test_io(self): - raise unittest.SkipTest('😊') - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines(["* 1 skipped *"]) - - -def test_raises(pytester: Pytester) -> None: - pytester.makepyfile( - """ - from nose.tools import raises - - @raises(RuntimeError) - def test_raises_runtimeerror(): - raise RuntimeError - - @raises(Exception) - def test_raises_baseexception_not_caught(): - raise BaseException - - @raises(BaseException) - def test_raises_baseexception_caught(): - raise BaseException - """ - ) - result = pytester.runpytest("-vv") - result.stdout.fnmatch_lines( - [ - "test_raises.py::test_raises_runtimeerror PASSED*", - "test_raises.py::test_raises_baseexception_not_caught FAILED*", - "test_raises.py::test_raises_baseexception_caught PASSED*", - "*= FAILURES =*", - "*_ test_raises_baseexception_not_caught _*", - "", - "arg = (), kw = {}", - "", - " def newfunc(*arg, **kw):", - " try:", - "> func(*arg, **kw)", - "", - "*/nose/*: ", - "_ _ *", - "", - " @raises(Exception)", - " def test_raises_baseexception_not_caught():", - "> raise BaseException", - "E BaseException", - "", - "test_raises.py:9: BaseException", - "* 1 failed, 2 passed *", - ] - ) - - -def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None: - """Regression test for #9391.""" - p = pytester.makepyfile( - __init__="", - setup=""" - """, - teardown=""" - """, - test_it=""" - from . import setup, teardown - - def test_it(): - pass - """, - ) - result = pytester.runpytest(p, "-p", "nose") - assert result.ret == 0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_parseopt.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_parseopt.py index 28529d04378..e959dfd631b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_parseopt.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_parseopt.py @@ -1,15 +1,17 @@ +# mypy: allow-untyped-defs import argparse +import locale import os +from pathlib import Path import shlex import subprocess import sys -from pathlib import Path -import pytest from _pytest.config import argparsing as parseopt from _pytest.config.exceptions import UsageError from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest @pytest.fixture @@ -53,9 +55,6 @@ class TestParser: assert argument.type is str argument = parseopt.Argument("-t", dest="abc", type=float) assert argument.type is float - with pytest.warns(DeprecationWarning): - with pytest.raises(KeyError): - argument = parseopt.Argument("-t", dest="abc", type="choice") argument = parseopt.Argument( "-t", dest="abc", type=str, choices=["red", "blue"] ) @@ -126,6 +125,17 @@ class TestParser: args = parser.parse([Path(".")]) assert getattr(args, parseopt.FILE_OR_DIR)[0] == "." + # Warning ignore because of: + # https://github.com/python/cpython/issues/85308 + # Can be removed once Python<3.12 support is dropped. + @pytest.mark.filterwarnings("ignore:'encoding' argument not specified") + def test_parse_from_file(self, parser: parseopt.Parser, tmp_path: Path) -> None: + tests = [".", "some.py::Test::test_method[param0]", "other/test_file.py"] + args_file = tmp_path / "tests.txt" + args_file.write_text("\n".join(tests), encoding="utf-8") + args = parser.parse([f"@{args_file.absolute()}"]) + assert getattr(args, parseopt.FILE_OR_DIR) == tests + def test_parse_known_args(self, parser: parseopt.Parser) -> None: parser.parse_known_args([Path(".")]) parser.addoption("--hello", action="store_true") @@ -289,13 +299,19 @@ class TestParser: def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + if sys.version_info >= (3, 11): + # New in Python 3.11, ignores utf-8 mode + encoding = locale.getencoding() + else: + encoding = locale.getpreferredencoding(False) try: bash_version = subprocess.run( ["bash", "--version"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=True, - universal_newlines=True, + text=True, + encoding=encoding, ).stdout except (OSError, subprocess.CalledProcessError): pytest.skip("bash is not available") @@ -305,14 +321,12 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: script = str(pytester.path.joinpath("test_argcomplete")) - with open(str(script), "w") as fp: + with open(str(script), "w", encoding="utf-8") as fp: # redirect output from argcomplete to stdin and stderr is not trivial # http://stackoverflow.com/q/12589419/1307905 # so we use bash fp.write( - 'COMP_WORDBREAKS="$COMP_WORDBREAKS" {} -m pytest 8>&1 9>&2'.format( - shlex.quote(sys.executable) - ) + f'COMP_WORDBREAKS="$COMP_WORDBREAKS" {shlex.quote(sys.executable)} -m pytest 8>&1 9>&2' ) # alternative would be extended Pytester.{run(),_run(),popen()} to be able # to handle a keyword argument env that replaces os.environ in popen or @@ -330,9 +344,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: pytest.skip("argcomplete not available") elif not result.stdout.str(): pytest.skip( - "bash provided no output on stdout, argcomplete not available? (stderr={!r})".format( - result.stderr.str() - ) + f"bash provided no output on stdout, argcomplete not available? (stderr={result.stderr.str()!r})" ) else: result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_pastebin.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_pastebin.py index b338519ae17..651a04da84a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_pastebin.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_pastebin.py @@ -1,10 +1,12 @@ +# mypy: allow-untyped-defs +import email.message import io from typing import List from typing import Union -import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest class TestPasteCapture: @@ -98,7 +100,9 @@ class TestPaste: def mocked(url, data): calls.append((url, data)) - raise urllib.error.HTTPError(url, 400, "Bad request", {}, io.BytesIO()) + raise urllib.error.HTTPError( + url, 400, "Bad request", email.message.Message(), io.BytesIO() + ) monkeypatch.setattr(urllib.request, "urlopen", mocked) return calls diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_pathlib.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_pathlib.py index 5eb153e847d..688d13f2f05 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_pathlib.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_pathlib.py @@ -1,30 +1,61 @@ +# mypy: allow-untyped-defs +import errno +import importlib.abc +import importlib.machinery import os.path +from pathlib import Path import pickle +import shutil import sys -import unittest.mock -from pathlib import Path from textwrap import dedent from types import ModuleType from typing import Any from typing import Generator +from typing import Iterator +from typing import Optional +from typing import Sequence +from typing import Tuple +import unittest.mock -import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import bestrelpath from _pytest.pathlib import commonpath +from _pytest.pathlib import compute_module_name +from _pytest.pathlib import CouldNotResolvePathError from _pytest.pathlib import ensure_deletable from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str from _pytest.pathlib import get_lock_path from _pytest.pathlib import import_path +from _pytest.pathlib import ImportMode from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import insert_missing_modules +from _pytest.pathlib import is_importable from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import module_name_from_path from _pytest.pathlib import resolve_package_path +from _pytest.pathlib import resolve_pkg_root_and_module_name +from _pytest.pathlib import safe_exists from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit +from _pytest.pytester import Pytester +from _pytest.pytester import RunResult from _pytest.tmpdir import TempPathFactory +import pytest + + +@pytest.fixture(autouse=True) +def autouse_pytester(pytester: Pytester) -> None: + """ + Fixture to make pytester() being autouse for all tests in this module. + + pytester makes sure to restore sys.path to its previous state, and many tests in this module + import modules and change sys.path because of that, so common module names such as "test" or "test.conftest" + end up leaking to tests in other modules. + + Note: we might consider extracting the sys.path restoration aspect into its own fixture, and apply it + to the entire test suite always. + """ class TestFNMatcherPort: @@ -76,6 +107,15 @@ class TestFNMatcherPort: assert not fnmatch_ex(pattern, path) +@pytest.fixture(params=[True, False]) +def ns_param(request: pytest.FixtureRequest) -> bool: + """ + Simple parametrized fixture for tests which call import_path() with consider_namespace_packages + using True and False. + """ + return bool(request.param) + + class TestImportPath: """ @@ -91,16 +131,22 @@ class TestImportPath: yield path assert path.joinpath("samplefile").exists() + @pytest.fixture(autouse=True) + def preserve_sys(self): + with unittest.mock.patch.dict(sys.modules): + with unittest.mock.patch.object(sys, "path", list(sys.path)): + yield + def setuptestfs(self, path: Path) -> None: # print "setting up test fs for", repr(path) samplefile = path / "samplefile" - samplefile.write_text("samplefile\n") + samplefile.write_text("samplefile\n", encoding="utf-8") execfile = path / "execfile" - execfile.write_text("x=42") + execfile.write_text("x=42", encoding="utf-8") execfilepy = path / "execfile.py" - execfilepy.write_text("x=42") + execfilepy.write_text("x=42", encoding="utf-8") d = {1: 2, "hello": "world", "answer": 42} path.joinpath("samplepickle").write_bytes(pickle.dumps(d, 1)) @@ -114,9 +160,9 @@ class TestImportPath: otherdir.joinpath("__init__.py").touch() module_a = otherdir / "a.py" - module_a.write_text("from .b import stuff as result\n") + module_a.write_text("from .b import stuff as result\n", encoding="utf-8") module_b = otherdir / "b.py" - module_b.write_text('stuff="got it"\n') + module_b.write_text('stuff="got it"\n', encoding="utf-8") module_c = otherdir / "c.py" module_c.write_text( dedent( @@ -125,7 +171,8 @@ class TestImportPath: import otherdir.a value = otherdir.a.result """ - ) + ), + encoding="utf-8", ) module_d = otherdir / "d.py" module_d.write_text( @@ -135,181 +182,249 @@ class TestImportPath: from otherdir import a value2 = a.result """ - ) + ), + encoding="utf-8", ) - def test_smoke_test(self, path1: Path) -> None: - obj = import_path(path1 / "execfile.py", root=path1) - assert obj.x == 42 # type: ignore[attr-defined] + def test_smoke_test(self, path1: Path, ns_param: bool) -> None: + obj = import_path( + path1 / "execfile.py", root=path1, consider_namespace_packages=ns_param + ) + assert obj.x == 42 assert obj.__name__ == "execfile" + def test_import_path_missing_file(self, path1: Path, ns_param: bool) -> None: + with pytest.raises(ImportPathMismatchError): + import_path( + path1 / "sampledir", root=path1, consider_namespace_packages=ns_param + ) + def test_renamed_dir_creates_mismatch( - self, tmp_path: Path, monkeypatch: MonkeyPatch + self, tmp_path: Path, monkeypatch: MonkeyPatch, ns_param: bool ) -> None: tmp_path.joinpath("a").mkdir() p = tmp_path.joinpath("a", "test_x123.py") p.touch() - import_path(p, root=tmp_path) + import_path(p, root=tmp_path, consider_namespace_packages=ns_param) tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) with pytest.raises(ImportPathMismatchError): - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=ns_param, + ) # Errors can be ignored. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=ns_param, + ) # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") with pytest.raises(ImportPathMismatchError): - import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) + import_path( + tmp_path.joinpath("b", "test_x123.py"), + root=tmp_path, + consider_namespace_packages=ns_param, + ) - def test_messy_name(self, tmp_path: Path) -> None: + def test_messy_name(self, tmp_path: Path, ns_param: bool) -> None: # https://bitbucket.org/hpk42/py-trunk/issue/129 path = tmp_path / "foo__init__.py" path.touch() - module = import_path(path, root=tmp_path) + module = import_path(path, root=tmp_path, consider_namespace_packages=ns_param) assert module.__name__ == "foo__init__" - def test_dir(self, tmp_path: Path) -> None: + def test_dir(self, tmp_path: Path, ns_param: bool) -> None: p = tmp_path / "hello_123" p.mkdir() p_init = p / "__init__.py" p_init.touch() - m = import_path(p, root=tmp_path) + m = import_path(p, root=tmp_path, consider_namespace_packages=ns_param) assert m.__name__ == "hello_123" - m = import_path(p_init, root=tmp_path) + m = import_path(p_init, root=tmp_path, consider_namespace_packages=ns_param) assert m.__name__ == "hello_123" - def test_a(self, path1: Path) -> None: + def test_a(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "a.py", root=path1) - assert mod.result == "got it" # type: ignore[attr-defined] + mod = import_path( + otherdir / "a.py", root=path1, consider_namespace_packages=ns_param + ) + assert mod.result == "got it" assert mod.__name__ == "otherdir.a" - def test_b(self, path1: Path) -> None: + def test_b(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "b.py", root=path1) - assert mod.stuff == "got it" # type: ignore[attr-defined] + mod = import_path( + otherdir / "b.py", root=path1, consider_namespace_packages=ns_param + ) + assert mod.stuff == "got it" assert mod.__name__ == "otherdir.b" - def test_c(self, path1: Path) -> None: + def test_c(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "c.py", root=path1) - assert mod.value == "got it" # type: ignore[attr-defined] + mod = import_path( + otherdir / "c.py", root=path1, consider_namespace_packages=ns_param + ) + assert mod.value == "got it" - def test_d(self, path1: Path) -> None: + def test_d(self, path1: Path, ns_param: bool) -> None: otherdir = path1 / "otherdir" - mod = import_path(otherdir / "d.py", root=path1) - assert mod.value2 == "got it" # type: ignore[attr-defined] + mod = import_path( + otherdir / "d.py", root=path1, consider_namespace_packages=ns_param + ) + assert mod.value2 == "got it" - def test_import_after(self, tmp_path: Path) -> None: + def test_import_after(self, tmp_path: Path, ns_param: bool) -> None: tmp_path.joinpath("xxxpackage").mkdir() tmp_path.joinpath("xxxpackage", "__init__.py").touch() mod1path = tmp_path.joinpath("xxxpackage", "module1.py") mod1path.touch() - mod1 = import_path(mod1path, root=tmp_path) + mod1 = import_path( + mod1path, root=tmp_path, consider_namespace_packages=ns_param + ) assert mod1.__name__ == "xxxpackage.module1" from xxxpackage import module1 assert module1 is mod1 def test_check_filepath_consistency( - self, monkeypatch: MonkeyPatch, tmp_path: Path + self, monkeypatch: MonkeyPatch, tmp_path: Path, ns_param: bool ) -> None: name = "pointsback123" p = tmp_path.joinpath(name + ".py") p.touch() - for ending in (".pyc", ".pyo"): - mod = ModuleType(name) - pseudopath = tmp_path.joinpath(name + ending) - pseudopath.touch() - mod.__file__ = str(pseudopath) - monkeypatch.setitem(sys.modules, name, mod) - newmod = import_path(p, root=tmp_path) - assert mod == newmod - monkeypatch.undo() + with monkeypatch.context() as mp: + for ending in (".pyc", ".pyo"): + mod = ModuleType(name) + pseudopath = tmp_path.joinpath(name + ending) + pseudopath.touch() + mod.__file__ = str(pseudopath) + mp.setitem(sys.modules, name, mod) + newmod = import_path( + p, root=tmp_path, consider_namespace_packages=ns_param + ) + assert mod == newmod mod = ModuleType(name) pseudopath = tmp_path.joinpath(name + "123.py") pseudopath.touch() mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) with pytest.raises(ImportPathMismatchError) as excinfo: - import_path(p, root=tmp_path) + import_path(p, root=tmp_path, consider_namespace_packages=ns_param) modname, modfile, orig = excinfo.value.args assert modname == name assert modfile == str(pseudopath) assert orig == p assert issubclass(ImportPathMismatchError, ImportError) - def test_issue131_on__init__(self, tmp_path: Path) -> None: - # __init__.py files may be namespace packages, and thus the - # __file__ of an imported module may not be ourselves - # see issue - tmp_path.joinpath("proja").mkdir() - p1 = tmp_path.joinpath("proja", "__init__.py") - p1.touch() - tmp_path.joinpath("sub", "proja").mkdir(parents=True) - p2 = tmp_path.joinpath("sub", "proja", "__init__.py") - p2.touch() - m1 = import_path(p1, root=tmp_path) - m2 = import_path(p2, root=tmp_path) - assert m1 == m2 - - def test_ensuresyspath_append(self, tmp_path: Path) -> None: + def test_ensuresyspath_append(self, tmp_path: Path, ns_param: bool) -> None: root1 = tmp_path / "root1" root1.mkdir() file1 = root1 / "x123.py" file1.touch() assert str(root1) not in sys.path - import_path(file1, mode="append", root=tmp_path) + import_path( + file1, mode="append", root=tmp_path, consider_namespace_packages=ns_param + ) assert str(root1) == sys.path[-1] assert str(root1) not in sys.path[:-1] - def test_invalid_path(self, tmp_path: Path) -> None: + def test_invalid_path(self, tmp_path: Path, ns_param: bool) -> None: with pytest.raises(ImportError): - import_path(tmp_path / "invalid.py", root=tmp_path) + import_path( + tmp_path / "invalid.py", + root=tmp_path, + consider_namespace_packages=ns_param, + ) @pytest.fixture - def simple_module(self, tmp_path: Path) -> Path: - fn = tmp_path / "_src/tests/mymod.py" + def simple_module( + self, tmp_path: Path, request: pytest.FixtureRequest + ) -> Iterator[Path]: + name = f"mymod_{request.node.name}" + fn = tmp_path / f"_src/tests/{name}.py" fn.parent.mkdir(parents=True) - fn.write_text("def foo(x): return 40 + x") - return fn - - def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None: + fn.write_text("def foo(x): return 40 + x", encoding="utf-8") + module_name = module_name_from_path(fn, root=tmp_path) + yield fn + sys.modules.pop(module_name, None) + + def test_importmode_importlib( + self, + simple_module: Path, + tmp_path: Path, + request: pytest.FixtureRequest, + ns_param: bool, + ) -> None: """`importlib` mode does not change sys.path.""" - module = import_path(simple_module, mode="importlib", root=tmp_path) - assert module.foo(2) == 42 # type: ignore[attr-defined] + module = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=ns_param, + ) + assert module.foo(2) == 42 assert str(simple_module.parent) not in sys.path assert module.__name__ in sys.modules - assert module.__name__ == "_src.tests.mymod" + assert module.__name__ == f"_src.tests.mymod_{request.node.name}" assert "_src" in sys.modules assert "_src.tests" in sys.modules - def test_importmode_twice_is_different_module( - self, simple_module: Path, tmp_path: Path + def test_remembers_previous_imports( + self, simple_module: Path, tmp_path: Path, ns_param: bool ) -> None: - """`importlib` mode always returns a new module.""" - module1 = import_path(simple_module, mode="importlib", root=tmp_path) - module2 = import_path(simple_module, mode="importlib", root=tmp_path) - assert module1 is not module2 + """`importlib` mode called remembers previous module (#10341, #10811).""" + module1 = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=ns_param, + ) + module2 = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=ns_param, + ) + assert module1 is module2 def test_no_meta_path_found( - self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path + self, + simple_module: Path, + monkeypatch: MonkeyPatch, + tmp_path: Path, + ns_param: bool, ) -> None: """Even without any meta_path should still import module.""" monkeypatch.setattr(sys, "meta_path", []) - module = import_path(simple_module, mode="importlib", root=tmp_path) - assert module.foo(2) == 42 # type: ignore[attr-defined] + module = import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=ns_param, + ) + assert module.foo(2) == 42 # mode='importlib' fails if no spec is found to load the module import importlib.util + # Force module to be re-imported. + del sys.modules[module.__name__] + monkeypatch.setattr( importlib.util, "spec_from_file_location", lambda *args: None ) with pytest.raises(ImportError): - import_path(simple_module, mode="importlib", root=tmp_path) + import_path( + simple_module, + mode="importlib", + root=tmp_path, + consider_namespace_packages=False, + ) def test_resolve_package_path(tmp_path: Path) -> None: @@ -319,18 +434,18 @@ def test_resolve_package_path(tmp_path: Path) -> None: (pkg / "subdir").mkdir() (pkg / "subdir/__init__.py").touch() assert resolve_package_path(pkg) == pkg - assert resolve_package_path(pkg.joinpath("subdir", "__init__.py")) == pkg + assert resolve_package_path(pkg / "subdir/__init__.py") == pkg def test_package_unimportable(tmp_path: Path) -> None: pkg = tmp_path / "pkg1-1" pkg.mkdir() pkg.joinpath("__init__.py").touch() - subdir = pkg.joinpath("subdir") + subdir = pkg / "subdir" subdir.mkdir() - pkg.joinpath("subdir/__init__.py").touch() + (pkg / "subdir/__init__.py").touch() assert resolve_package_path(subdir) == subdir - xyz = subdir.joinpath("xyz.py") + xyz = subdir / "xyz.py" xyz.touch() assert resolve_package_path(xyz) == subdir assert not resolve_package_path(pkg) @@ -437,7 +552,7 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N return False, even when they are clearly equal. """ module_path = tmp_path.joinpath("my_module.py") - module_path.write_text("def foo(): return 42") + module_path.write_text("def foo(): return 42", encoding="utf-8") monkeypatch.syspath_prepend(tmp_path) with monkeypatch.context() as mp: @@ -445,13 +560,16 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N # the paths too. Using a context to narrow the patch as much as possible given # this is an important system function. mp.setattr(os.path, "samefile", lambda x, y: False) - module = import_path(module_path, root=tmp_path) + module = import_path( + module_path, root=tmp_path, consider_namespace_packages=False + ) assert getattr(module, "foo")() == 42 class TestImportLibMode: - @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") - def test_importmode_importlib_with_dataclass(self, tmp_path: Path) -> None: + def test_importmode_importlib_with_dataclass( + self, tmp_path: Path, ns_param: bool + ) -> None: """Ensure that importlib mode works with a module containing dataclasses (#7856).""" fn = tmp_path.joinpath("_src/tests/test_dataclass.py") fn.parent.mkdir(parents=True) @@ -464,16 +582,27 @@ class TestImportLibMode: class Data: value: str """ - ) + ), + encoding="utf-8", ) - module = import_path(fn, mode="importlib", root=tmp_path) + module = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) Data: Any = getattr(module, "Data") data = Data(value="foo") assert data.value == "foo" assert data.__module__ == "_src.tests.test_dataclass" - def test_importmode_importlib_with_pickle(self, tmp_path: Path) -> None: + # Ensure we do not import the same module again (#11475). + module2 = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) + assert module is module2 + + def test_importmode_importlib_with_pickle( + self, tmp_path: Path, ns_param: bool + ) -> None: """Ensure that importlib mode works with pickle (#7859).""" fn = tmp_path.joinpath("_src/tests/test_pickle.py") fn.parent.mkdir(parents=True) @@ -489,16 +618,25 @@ class TestImportLibMode: s = pickle.dumps(_action) return pickle.loads(s) """ - ) + ), + encoding="utf-8", ) - module = import_path(fn, mode="importlib", root=tmp_path) + module = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) round_trip = getattr(module, "round_trip") action = round_trip() assert action() == 42 + # Ensure we do not import the same module again (#11475). + module2 = import_path( + fn, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) + assert module is module2 + def test_importmode_importlib_with_pickle_separate_modules( - self, tmp_path: Path + self, tmp_path: Path, ns_param: bool ) -> None: """ Ensure that importlib mode works can load pickles that look similar but are @@ -509,14 +647,15 @@ class TestImportLibMode: fn1.write_text( dedent( """ - import attr + import dataclasses import pickle - @attr.s(auto_attribs=True) + @dataclasses.dataclass class Data: x: int = 42 """ - ) + ), + encoding="utf-8", ) fn2 = tmp_path.joinpath("_src/m2/tests/test.py") @@ -524,14 +663,15 @@ class TestImportLibMode: fn2.write_text( dedent( """ - import attr + import dataclasses import pickle - @attr.s(auto_attribs=True) + @dataclasses.dataclass class Data: x: str = "" """ - ) + ), + encoding="utf-8", ) import pickle @@ -540,10 +680,14 @@ class TestImportLibMode: s = pickle.dumps(obj) return pickle.loads(s) - module = import_path(fn1, mode="importlib", root=tmp_path) + module = import_path( + fn1, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) Data1 = getattr(module, "Data") - module = import_path(fn2, mode="importlib", root=tmp_path) + module = import_path( + fn2, mode="importlib", root=tmp_path, consider_namespace_packages=ns_param + ) Data2 = getattr(module, "Data") assert round_trip(Data1(20)) == Data1(20) @@ -559,16 +703,864 @@ class TestImportLibMode: result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar")) assert result == "home.foo.test_foo" - def test_insert_missing_modules(self) -> None: - modules = {"src.tests.foo": ModuleType("src.tests.foo")} - insert_missing_modules(modules, "src.tests.foo") - assert sorted(modules) == ["src", "src.tests", "src.tests.foo"] + # Importing __init__.py files should return the package as module name. + result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path) + assert result == "src.app" + + # Unless __init__.py file is at the root, in which case we cannot have an empty module name. + result = module_name_from_path(tmp_path / "__init__.py", tmp_path) + assert result == "__init__" + + # Modules which start with "." are considered relative and will not be imported + # unless part of a package, so we replace it with a "_" when generating the fake module name. + result = module_name_from_path(tmp_path / ".env/tests/test_foo.py", tmp_path) + assert result == "_env.tests.test_foo" + + # We want to avoid generating extra intermediate modules if some directory just happens + # to contain a "." in the name. + result = module_name_from_path( + tmp_path / ".env.310/tests/test_foo.py", tmp_path + ) + assert result == "_env_310.tests.test_foo" + + def test_resolve_pkg_root_and_module_name( + self, tmp_path: Path, monkeypatch: MonkeyPatch, pytester: Pytester + ) -> None: + # Create a directory structure first without __init__.py files. + (tmp_path / "src/app/core").mkdir(parents=True) + models_py = tmp_path / "src/app/core/models.py" + models_py.touch() + + with pytest.raises(CouldNotResolvePathError): + _ = resolve_pkg_root_and_module_name(models_py) + + # Create the __init__.py files, it should now resolve to a proper module name. + (tmp_path / "src/app/__init__.py").touch() + (tmp_path / "src/app/core/__init__.py").touch() + assert resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) == ( + tmp_path / "src", + "app.core.models", + ) + + # If we add tmp_path to sys.path, src becomes a namespace package. + monkeypatch.syspath_prepend(tmp_path) + validate_namespace_package(pytester, [tmp_path], ["src.app.core.models"]) + + assert resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) == ( + tmp_path, + "src.app.core.models", + ) + assert resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=False + ) == ( + tmp_path / "src", + "app.core.models", + ) + + def test_insert_missing_modules( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ) -> None: + monkeypatch.chdir(tmp_path) + # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and + # don't end up being imported. + modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")} + insert_missing_modules(modules, "xxx.tests.foo") + assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"] mod = ModuleType("mod", doc="My Module") - modules = {"src": mod} - insert_missing_modules(modules, "src") - assert modules == {"src": mod} + modules = {"xxy": mod} + insert_missing_modules(modules, "xxy") + assert modules == {"xxy": mod} modules = {} insert_missing_modules(modules, "") assert modules == {} + + def test_parent_contains_child_module_attribute( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ): + monkeypatch.chdir(tmp_path) + # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and + # don't end up being imported. + modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")} + insert_missing_modules(modules, "xxx.tests.foo") + assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"] + assert modules["xxx"].tests is modules["xxx.tests"] + assert modules["xxx.tests"].foo is modules["xxx.tests.foo"] + + def test_importlib_package( + self, monkeypatch: MonkeyPatch, tmp_path: Path, ns_param: bool + ): + """ + Importing a package using --importmode=importlib should not import the + package's __init__.py file more than once (#11306). + """ + monkeypatch.chdir(tmp_path) + monkeypatch.syspath_prepend(tmp_path) + + package_name = "importlib_import_package" + tmp_path.joinpath(package_name).mkdir() + init = tmp_path.joinpath(f"{package_name}/__init__.py") + init.write_text( + dedent( + """ + from .singleton import Singleton + + instance = Singleton() + """ + ), + encoding="ascii", + ) + singleton = tmp_path.joinpath(f"{package_name}/singleton.py") + singleton.write_text( + dedent( + """ + class Singleton: + INSTANCES = [] + + def __init__(self) -> None: + self.INSTANCES.append(self) + if len(self.INSTANCES) > 1: + raise RuntimeError("Already initialized") + """ + ), + encoding="ascii", + ) + + mod = import_path( + init, + root=tmp_path, + mode=ImportMode.importlib, + consider_namespace_packages=ns_param, + ) + assert len(mod.instance.INSTANCES) == 1 + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + init, + root=tmp_path, + mode=ImportMode.importlib, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + + def test_importlib_root_is_package(self, pytester: Pytester) -> None: + """ + Regression for importing a `__init__`.py file that is at the root + (#11417). + """ + pytester.makepyfile(__init__="") + pytester.makepyfile( + """ + def test_my_test(): + assert True + """ + ) + + result = pytester.runpytest("--import-mode=importlib") + result.stdout.fnmatch_lines("* 1 passed *") + + def create_installed_doctests_and_tests_dir( + self, path: Path, monkeypatch: MonkeyPatch + ) -> Tuple[Path, Path, Path]: + """ + Create a directory structure where the application code is installed in a virtual environment, + and the tests are in an outside ".tests" directory. + + Return the paths to the core module (installed in the virtualenv), and the test modules. + """ + app = path / "src/app" + app.mkdir(parents=True) + (app / "__init__.py").touch() + core_py = app / "core.py" + core_py.write_text( + dedent( + """ + def foo(): + ''' + >>> 1 + 1 + 2 + ''' + """ + ), + encoding="ascii", + ) + + # Install it into a site-packages directory, and add it to sys.path, mimicking what + # happens when installing into a virtualenv. + site_packages = path / ".env/lib/site-packages" + site_packages.mkdir(parents=True) + shutil.copytree(app, site_packages / "app") + assert (site_packages / "app/core.py").is_file() + + monkeypatch.syspath_prepend(site_packages) + + # Create the tests files, outside 'src' and the virtualenv. + # We use the same test name on purpose, but in different directories, to ensure + # this works as advertised. + conftest_path1 = path / ".tests/a/conftest.py" + conftest_path1.parent.mkdir(parents=True) + conftest_path1.write_text( + dedent( + """ + import pytest + @pytest.fixture + def a_fix(): return "a" + """ + ), + encoding="ascii", + ) + test_path1 = path / ".tests/a/test_core.py" + test_path1.write_text( + dedent( + """ + import app.core + def test(a_fix): + assert a_fix == "a" + """, + ), + encoding="ascii", + ) + + conftest_path2 = path / ".tests/b/conftest.py" + conftest_path2.parent.mkdir(parents=True) + conftest_path2.write_text( + dedent( + """ + import pytest + @pytest.fixture + def b_fix(): return "b" + """ + ), + encoding="ascii", + ) + + test_path2 = path / ".tests/b/test_core.py" + test_path2.write_text( + dedent( + """ + import app.core + def test(b_fix): + assert b_fix == "b" + """, + ), + encoding="ascii", + ) + return (site_packages / "app/core.py"), test_path1, test_path2 + + def test_import_using_normal_mechanism_first( + self, monkeypatch: MonkeyPatch, pytester: Pytester, ns_param: bool + ) -> None: + """ + Test import_path imports from the canonical location when possible first, only + falling back to its normal flow when the module being imported is not reachable via sys.path (#11475). + """ + core_py, test_path1, test_path2 = self.create_installed_doctests_and_tests_dir( + pytester.path, monkeypatch + ) + + # core_py is reached from sys.path, so should be imported normally. + mod = import_path( + core_py, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod.__name__ == "app.core" + assert mod.__file__ and Path(mod.__file__) == core_py + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + core_py, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + + # tests are not reachable from sys.path, so they are imported as a standalone modules. + # Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because + # importlib considers module names starting with '.' to be local imports. + mod = import_path( + test_path1, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod.__name__ == "_tests.a.test_core" + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + test_path1, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + + mod = import_path( + test_path2, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod.__name__ == "_tests.b.test_core" + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + test_path2, + mode="importlib", + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + + def test_import_using_normal_mechanism_first_integration( + self, monkeypatch: MonkeyPatch, pytester: Pytester, ns_param: bool + ) -> None: + """ + Same test as above, but verify the behavior calling pytest. + + We should not make this call in the same test as above, as the modules have already + been imported by separate import_path() calls. + """ + core_py, test_path1, test_path2 = self.create_installed_doctests_and_tests_dir( + pytester.path, monkeypatch + ) + result = pytester.runpytest( + "--import-mode=importlib", + "-o", + f"consider_namespace_packages={ns_param}", + "--doctest-modules", + "--pyargs", + "app", + "./.tests", + ) + result.stdout.fnmatch_lines( + [ + f"{core_py.relative_to(pytester.path)} . *", + f"{test_path1.relative_to(pytester.path)} . *", + f"{test_path2.relative_to(pytester.path)} . *", + "* 3 passed*", + ] + ) + + def test_import_path_imports_correct_file( + self, pytester: Pytester, ns_param: bool + ) -> None: + """ + Import the module by the given path, even if other module with the same name + is reachable from sys.path. + """ + pytester.syspathinsert() + # Create a 'x.py' module reachable from sys.path that raises AssertionError + # if imported. + x_at_root = pytester.path / "x.py" + x_at_root.write_text("raise AssertionError('x at root')", encoding="ascii") + + # Create another x.py module, but in some subdirectories to ensure it is not + # accessible from sys.path. + x_in_sub_folder = pytester.path / "a/b/x.py" + x_in_sub_folder.parent.mkdir(parents=True) + x_in_sub_folder.write_text("X = 'a/b/x'", encoding="ascii") + + # Import our x.py module from the subdirectories. + # The 'x.py' module from sys.path was not imported for sure because + # otherwise we would get an AssertionError. + mod = import_path( + x_in_sub_folder, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder + assert mod.X == "a/b/x" + + mod2 = import_path( + x_in_sub_folder, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + assert mod is mod2 + + # Attempt to import root 'x.py'. + with pytest.raises(AssertionError, match="x at root"): + _ = import_path( + x_at_root, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + + +def test_safe_exists(tmp_path: Path) -> None: + d = tmp_path.joinpath("some_dir") + d.mkdir() + assert safe_exists(d) is True + + f = tmp_path.joinpath("some_file") + f.touch() + assert safe_exists(f) is True + + # Use unittest.mock() as a context manager to have a very narrow + # patch lifetime. + p = tmp_path.joinpath("some long filename" * 100) + with unittest.mock.patch.object( + Path, + "exists", + autospec=True, + side_effect=OSError(errno.ENAMETOOLONG, "name too long"), + ): + assert safe_exists(p) is False + + with unittest.mock.patch.object( + Path, + "exists", + autospec=True, + side_effect=ValueError("name too long"), + ): + assert safe_exists(p) is False + + +def test_import_sets_module_as_attribute(pytester: Pytester) -> None: + """Unittest test for #12194.""" + pytester.path.joinpath("foo/bar/baz").mkdir(parents=True) + pytester.path.joinpath("foo/__init__.py").touch() + pytester.path.joinpath("foo/bar/__init__.py").touch() + pytester.path.joinpath("foo/bar/baz/__init__.py").touch() + pytester.syspathinsert() + + # Import foo.bar.baz and ensure parent modules also ended up imported. + baz = import_path( + pytester.path.joinpath("foo/bar/baz/__init__.py"), + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + assert baz.__name__ == "foo.bar.baz" + foo = sys.modules["foo"] + assert foo.__name__ == "foo" + bar = sys.modules["foo.bar"] + assert bar.__name__ == "foo.bar" + + # Check parent modules have an attribute pointing to their children. + assert bar.baz is baz + assert foo.bar is bar + + # Ensure we returned the "foo.bar" module cached in sys.modules. + bar_2 = import_path( + pytester.path.joinpath("foo/bar/__init__.py"), + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + assert bar_2 is bar + + +def test_import_sets_module_as_attribute_without_init_files(pytester: Pytester) -> None: + """Similar to test_import_sets_module_as_attribute, but without __init__.py files.""" + pytester.path.joinpath("foo/bar").mkdir(parents=True) + pytester.path.joinpath("foo/bar/baz.py").touch() + pytester.syspathinsert() + + # Import foo.bar.baz and ensure parent modules also ended up imported. + baz = import_path( + pytester.path.joinpath("foo/bar/baz.py"), + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + assert baz.__name__ == "foo.bar.baz" + foo = sys.modules["foo"] + assert foo.__name__ == "foo" + bar = sys.modules["foo.bar"] + assert bar.__name__ == "foo.bar" + + # Check parent modules have an attribute pointing to their children. + assert bar.baz is baz + assert foo.bar is bar + + # Ensure we returned the "foo.bar.baz" module cached in sys.modules. + baz_2 = import_path( + pytester.path.joinpath("foo/bar/baz.py"), + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + assert baz_2 is baz + + +def test_import_sets_module_as_attribute_regression(pytester: Pytester) -> None: + """Regression test for #12194.""" + pytester.path.joinpath("foo/bar/baz").mkdir(parents=True) + pytester.path.joinpath("foo/__init__.py").touch() + pytester.path.joinpath("foo/bar/__init__.py").touch() + pytester.path.joinpath("foo/bar/baz/__init__.py").touch() + f = pytester.makepyfile( + """ + import foo + from foo.bar import baz + foo.bar.baz + + def test_foo() -> None: + pass + """ + ) + + pytester.syspathinsert() + result = pytester.runpython(f) + assert result.ret == 0 + + result = pytester.runpytest("--import-mode=importlib", "--doctest-modules") + assert result.ret == 0 + + +def test_import_submodule_not_namespace(pytester: Pytester) -> None: + """ + Regression test for importing a submodule 'foo.bar' while there is a 'bar' directory + reachable from sys.path -- ensuring the top-level module does not end up imported as a namespace + package. + + #12194 + https://github.com/pytest-dev/pytest/pull/12208#issuecomment-2056458432 + """ + pytester.syspathinsert() + # Create package 'foo' with a submodule 'bar'. + pytester.path.joinpath("foo").mkdir() + foo_path = pytester.path.joinpath("foo/__init__.py") + foo_path.touch() + bar_path = pytester.path.joinpath("foo/bar.py") + bar_path.touch() + # Create top-level directory in `sys.path` with the same name as that submodule. + pytester.path.joinpath("bar").mkdir() + + # Import `foo`, then `foo.bar`, and check they were imported from the correct location. + foo = import_path( + foo_path, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + bar = import_path( + bar_path, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=False, + ) + assert foo.__name__ == "foo" + assert bar.__name__ == "foo.bar" + assert foo.__file__ is not None + assert bar.__file__ is not None + assert Path(foo.__file__) == foo_path + assert Path(bar.__file__) == bar_path + + +class TestNamespacePackages: + """Test import_path support when importing from properly namespace packages.""" + + @pytest.fixture(autouse=True) + def setup_imports_tracking(self, monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr(sys, "pytest_namespace_packages_test", [], raising=False) + + def setup_directories( + self, tmp_path: Path, monkeypatch: Optional[MonkeyPatch], pytester: Pytester + ) -> Tuple[Path, Path]: + # Use a code to guard against modules being imported more than once. + # This is a safeguard in case future changes break this invariant. + code = dedent( + """ + import sys + imported = getattr(sys, "pytest_namespace_packages_test", []) + assert __name__ not in imported, f"{__name__} already imported" + imported.append(__name__) + sys.pytest_namespace_packages_test = imported + """ + ) + + # Set up a namespace package "com.company", containing + # two subpackages, "app" and "calc". + (tmp_path / "src/dist1/com/company/app/core").mkdir(parents=True) + (tmp_path / "src/dist1/com/company/app/__init__.py").write_text( + code, encoding="UTF-8" + ) + (tmp_path / "src/dist1/com/company/app/core/__init__.py").write_text( + code, encoding="UTF-8" + ) + models_py = tmp_path / "src/dist1/com/company/app/core/models.py" + models_py.touch() + + (tmp_path / "src/dist2/com/company/calc/algo").mkdir(parents=True) + (tmp_path / "src/dist2/com/company/calc/__init__.py").write_text( + code, encoding="UTF-8" + ) + (tmp_path / "src/dist2/com/company/calc/algo/__init__.py").write_text( + code, encoding="UTF-8" + ) + algorithms_py = tmp_path / "src/dist2/com/company/calc/algo/algorithms.py" + algorithms_py.write_text(code, encoding="UTF-8") + + r = validate_namespace_package( + pytester, + [tmp_path / "src/dist1", tmp_path / "src/dist2"], + ["com.company.app.core.models", "com.company.calc.algo.algorithms"], + ) + assert r.ret == 0 + if monkeypatch is not None: + monkeypatch.syspath_prepend(tmp_path / "src/dist1") + monkeypatch.syspath_prepend(tmp_path / "src/dist2") + return models_py, algorithms_py + + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) + def test_resolve_pkg_root_and_module_name_ns_multiple_levels( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + import_mode: str, + ) -> None: + models_py, algorithms_py = self.setup_directories( + tmp_path, monkeypatch, pytester + ) + + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1", + "com.company.app.core.models", + ) + + mod = import_path( + models_py, mode=import_mode, root=tmp_path, consider_namespace_packages=True + ) + assert mod.__name__ == "com.company.app.core.models" + assert mod.__file__ == str(models_py) + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + models_py, mode=import_mode, root=tmp_path, consider_namespace_packages=True + ) + assert mod is mod2 + + pkg_root, module_name = resolve_pkg_root_and_module_name( + algorithms_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist2", + "com.company.calc.algo.algorithms", + ) + + mod = import_path( + algorithms_py, + mode=import_mode, + root=tmp_path, + consider_namespace_packages=True, + ) + assert mod.__name__ == "com.company.calc.algo.algorithms" + assert mod.__file__ == str(algorithms_py) + + # Ensure we do not import the same module again (#11475). + mod2 = import_path( + algorithms_py, + mode=import_mode, + root=tmp_path, + consider_namespace_packages=True, + ) + assert mod is mod2 + + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) + def test_incorrect_namespace_package( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + import_mode: str, + ) -> None: + models_py, algorithms_py = self.setup_directories( + tmp_path, monkeypatch, pytester + ) + # Namespace packages must not have an __init__.py at its top-level + # directory; if it does, it is no longer a namespace package, and we fall back + # to importing just the part of the package containing the __init__.py files. + (tmp_path / "src/dist1/com/__init__.py").touch() + + # Because of the __init__ file, 'com' is no longer a namespace package: + # 'com.company.app' is importable as a normal module. + # 'com.company.calc' is no longer importable because 'com' is not a namespace package anymore. + r = validate_namespace_package( + pytester, + [tmp_path / "src/dist1", tmp_path / "src/dist2"], + ["com.company.app.core.models", "com.company.calc.algo.algorithms"], + ) + assert r.ret == 1 + r.stderr.fnmatch_lines("*No module named 'com.company.calc*") + + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1", + "com.company.app.core.models", + ) + + # dist2/com/company will contain a normal Python package. + pkg_root, module_name = resolve_pkg_root_and_module_name( + algorithms_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist2/com/company", + "calc.algo.algorithms", + ) + + def test_detect_meta_path( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + ) -> None: + """ + resolve_pkg_root_and_module_name() considers sys.meta_path when importing namespace packages. + + Regression test for #12112. + """ + + class CustomImporter(importlib.abc.MetaPathFinder): + """ + Imports the module name "com" as a namespace package. + + This ensures our namespace detection considers sys.meta_path, which is important + to support all possible ways a module can be imported (for example editable installs). + """ + + def find_spec( + self, name: str, path: Any = None, target: Any = None + ) -> Optional[importlib.machinery.ModuleSpec]: + if name == "com": + spec = importlib.machinery.ModuleSpec("com", loader=None) + spec.submodule_search_locations = [str(com_root_2), str(com_root_1)] + return spec + return None + + # Setup directories without configuring sys.path. + models_py, algorithms_py = self.setup_directories( + tmp_path, monkeypatch=None, pytester=pytester + ) + com_root_1 = tmp_path / "src/dist1/com" + com_root_2 = tmp_path / "src/dist2/com" + + # Because the namespace package is not setup correctly, we cannot resolve it as a namespace package. + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1/com/company", + "app.core.models", + ) + + # Insert our custom importer, which will recognize the "com" directory as a namespace package. + new_meta_path = [CustomImporter(), *sys.meta_path] + monkeypatch.setattr(sys, "meta_path", new_meta_path) + + # Now we should be able to resolve the path as namespace package. + pkg_root, module_name = resolve_pkg_root_and_module_name( + models_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist1", + "com.company.app.core.models", + ) + + @pytest.mark.parametrize("insert", [True, False]) + def test_full_ns_packages_without_init_files( + self, pytester: Pytester, tmp_path: Path, monkeypatch: MonkeyPatch, insert: bool + ) -> None: + (tmp_path / "src/dist1/ns/b/app/bar/test").mkdir(parents=True) + (tmp_path / "src/dist1/ns/b/app/bar/m.py").touch() + + if insert: + # The presence of this __init__.py is not a problem, ns.b.app is still part of the namespace package. + (tmp_path / "src/dist1/ns/b/app/__init__.py").touch() + + (tmp_path / "src/dist2/ns/a/core/foo/test").mkdir(parents=True) + (tmp_path / "src/dist2/ns/a/core/foo/m.py").touch() + + # Validate the namespace package by importing it in a Python subprocess. + r = validate_namespace_package( + pytester, + [tmp_path / "src/dist1", tmp_path / "src/dist2"], + ["ns.b.app.bar.m", "ns.a.core.foo.m"], + ) + assert r.ret == 0 + monkeypatch.syspath_prepend(tmp_path / "src/dist1") + monkeypatch.syspath_prepend(tmp_path / "src/dist2") + + assert resolve_pkg_root_and_module_name( + tmp_path / "src/dist1/ns/b/app/bar/m.py", consider_namespace_packages=True + ) == (tmp_path / "src/dist1", "ns.b.app.bar.m") + assert resolve_pkg_root_and_module_name( + tmp_path / "src/dist2/ns/a/core/foo/m.py", consider_namespace_packages=True + ) == (tmp_path / "src/dist2", "ns.a.core.foo.m") + + +def test_is_importable(pytester: Pytester) -> None: + pytester.syspathinsert() + + path = pytester.path / "bar/foo.py" + path.parent.mkdir() + path.touch() + assert is_importable("bar.foo", path) is True + + # Ensure that the module that can be imported points to the path we expect. + path = pytester.path / "some/other/path/bar/foo.py" + path.mkdir(parents=True, exist_ok=True) + assert is_importable("bar.foo", path) is False + + # Paths containing "." cannot be imported. + path = pytester.path / "bar.x/__init__.py" + path.parent.mkdir() + path.touch() + assert is_importable("bar.x", path) is False + + # Pass starting with "." denote relative imports and cannot be checked using is_importable. + path = pytester.path / ".bar.x/__init__.py" + path.parent.mkdir() + path.touch() + assert is_importable(".bar.x", path) is False + + +def test_compute_module_name(tmp_path: Path) -> None: + assert compute_module_name(tmp_path, tmp_path) is None + assert compute_module_name(Path(), Path()) is None + + assert compute_module_name(tmp_path, tmp_path / "mod.py") == "mod" + assert compute_module_name(tmp_path, tmp_path / "src/app/bar") == "src.app.bar" + assert compute_module_name(tmp_path, tmp_path / "src/app/bar.py") == "src.app.bar" + assert ( + compute_module_name(tmp_path, tmp_path / "src/app/bar/__init__.py") + == "src.app.bar" + ) + + +def validate_namespace_package( + pytester: Pytester, paths: Sequence[Path], modules: Sequence[str] +) -> RunResult: + """ + Validate that a Python namespace package is set up correctly. + + In a sub interpreter, add 'paths' to sys.path and attempt to import the given modules. + + In this module many tests configure a set of files as a namespace package, this function + is used as sanity check that our files are configured correctly from the point of view of Python. + """ + lines = [ + "import sys", + # Configure sys.path. + *[f"sys.path.append(r{str(x)!r})" for x in paths], + # Imports. + *[f"import {x}" for x in modules], + ] + return pytester.runpython_c("\n".join(lines)) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_pluginmanager.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_pluginmanager.py index 9fe23d17792..99b003b66ed 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_pluginmanager.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_pluginmanager.py @@ -1,10 +1,10 @@ +# mypy: allow-untyped-defs import os import shutil import sys import types from typing import List -import pytest from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -13,6 +13,7 @@ from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import import_path from _pytest.pytester import Pytester +import pytest @pytest.fixture @@ -45,7 +46,10 @@ class TestPytestPluginInteractions: kwargs=dict(pluginmanager=config.pluginmanager) ) config.pluginmanager._importconftest( - conf, importmode="prepend", rootpath=pytester.path + conf, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) # print(config.pluginmanager.get_plugins()) res = config.hook.pytest_myhook(xyz=10) @@ -74,7 +78,10 @@ class TestPytestPluginInteractions: """ ) config.pluginmanager._importconftest( - p, importmode="prepend", rootpath=pytester.path + p, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) assert config.option.test123 @@ -98,6 +105,40 @@ class TestPytestPluginInteractions: config.pluginmanager.register(A()) assert len(values) == 2 + @pytest.mark.skipif( + not sys.platform.startswith("win"), + reason="requires a case-insensitive file system", + ) + def test_conftestpath_case_sensitivity(self, pytester: Pytester) -> None: + """Unit test for issue #9765.""" + config = pytester.parseconfig() + pytester.makepyfile(**{"tests/conftest.py": ""}) + + conftest = pytester.path.joinpath("tests/conftest.py") + conftest_upper_case = pytester.path.joinpath("TESTS/conftest.py") + + mod = config.pluginmanager._importconftest( + conftest, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, + ) + plugin = config.pluginmanager.get_plugin(str(conftest)) + assert plugin is mod + + mod_uppercase = config.pluginmanager._importconftest( + conftest_upper_case, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, + ) + plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case)) + assert plugin_uppercase is mod_uppercase + + # No str(conftestpath) normalization so conftest should be imported + # twice and modules should be different objects + assert mod is not mod_uppercase + def test_hook_tracing(self, _config_for_test: Config) -> None: pytestpm = _config_for_test.pluginmanager # fully initialized with plugins saveindent = [] @@ -141,12 +182,18 @@ class TestPytestPluginInteractions: conftest2 = pytester.path.joinpath("tests/subdir/conftest.py") config.pluginmanager._importconftest( - conftest1, importmode="prepend", rootpath=pytester.path + conftest1, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) ihook_a = session.gethookproxy(pytester.path / "tests") assert ihook_a is not None config.pluginmanager._importconftest( - conftest2, importmode="prepend", rootpath=pytester.path + conftest2, + importmode="prepend", + rootpath=pytester.path, + consider_namespace_packages=False, ) ihook_b = session.gethookproxy(pytester.path / "tests") assert ihook_a is not ihook_b @@ -242,8 +289,12 @@ class TestPytestPluginManager: mod = types.ModuleType("temp") mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"] pytestpm.consider_module(mod) - assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1" - assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2" + p1 = pytestpm.get_plugin("pytest_p1") + assert p1 is not None + assert p1.__name__ == "pytest_p1" + p2 = pytestpm.get_plugin("pytest_p2") + assert p2 is not None + assert p2.__name__ == "pytest_p2" def test_consider_module_import_module( self, pytester: Pytester, _config_for_test: Config @@ -336,6 +387,7 @@ class TestPytestPluginManager: len2 = len(pytestpm.get_plugins()) assert len1 == len2 plugin1 = pytestpm.get_plugin("pytest_hello") + assert plugin1 is not None assert plugin1.__name__.endswith("pytest_hello") plugin2 = pytestpm.get_plugin("pytest_hello") assert plugin2 is plugin1 @@ -347,10 +399,11 @@ class TestPytestPluginManager: pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y") pytester.syspathinsert() - pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3") + pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3", encoding="utf-8") pluginname = "pkg.plug" pytestpm.import_plugin(pluginname) mod = pytestpm.get_plugin("pkg.plug") + assert mod is not None assert mod.x == 3 def test_consider_conftest_deps( @@ -359,13 +412,15 @@ class TestPytestPluginManager: pytestpm: PytestPluginManager, ) -> None: mod = import_path( - pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path + pytester.makepyfile("pytest_plugins='xyz'"), + root=pytester.path, + consider_namespace_packages=False, ) with pytest.raises(ImportError): - pytestpm.consider_conftest(mod) + pytestpm.consider_conftest(mod, registration_name="unused") -class TestPytestPluginManagerBootstrapming: +class TestPytestPluginManagerBootstrapping: def test_preparse_args(self, pytestpm: PytestPluginManager) -> None: pytest.raises( ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"]) @@ -391,7 +446,7 @@ class TestPytestPluginManagerBootstrapming: assert len(l2) == len(l1) assert 42 not in l2 - def test_plugin_prevent_register_unregistered_alredy_registered( + def test_plugin_prevent_register_unregistered_already_registered( self, pytestpm: PytestPluginManager ) -> None: pytestpm.register(42, name="abc") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_pytester.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_pytester.py index 049f8b22d81..9c6081a56db 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_pytester.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_pytester.py @@ -1,22 +1,21 @@ +# mypy: allow-untyped-defs import os import subprocess import sys import time -from pathlib import Path from types import ModuleType from typing import List -import _pytest.pytester as pytester_mod -import pytest from _pytest.config import ExitCode from _pytest.config import PytestPluginManager from _pytest.monkeypatch import MonkeyPatch -from _pytest.pytester import CwdSnapshot +import _pytest.pytester as pytester_mod from _pytest.pytester import HookRecorder from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot +import pytest def test_make_hook_recorder(pytester: Pytester) -> None: @@ -222,13 +221,13 @@ class TestInlineRunModulesCleanup: result = pytester.inline_run(str(test_mod)) assert result.ret == ExitCode.OK # rewrite module, now test should fail if module was re-imported - test_mod.write_text("def test_foo(): assert False") + test_mod.write_text("def test_foo(): assert False", encoding="utf-8") result2 = pytester.inline_run(str(test_mod)) assert result2.ret == ExitCode.TESTS_FAILED def spy_factory(self): class SysModulesSnapshotSpy: - instances: List["SysModulesSnapshotSpy"] = [] # noqa: F821 + instances: List["SysModulesSnapshotSpy"] = [] def __init__(self, preserve=None) -> None: SysModulesSnapshotSpy.instances.append(self) @@ -301,17 +300,6 @@ def test_assert_outcomes_after_pytest_error(pytester: Pytester) -> None: result.assert_outcomes(passed=0) -def test_cwd_snapshot(pytester: Pytester) -> None: - foo = pytester.mkdir("foo") - bar = pytester.mkdir("bar") - os.chdir(foo) - snapshot = CwdSnapshot() - os.chdir(bar) - assert Path().absolute() == bar - snapshot.restore() - assert Path().absolute() == foo - - class TestSysModulesSnapshot: key = "my-test-module" @@ -618,14 +606,9 @@ def test_linematcher_string_api() -> None: def test_pytest_addopts_before_pytester(request, monkeypatch: MonkeyPatch) -> None: - orig = os.environ.get("PYTEST_ADDOPTS", None) monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") - pytester: Pytester = request.getfixturevalue("pytester") + _: Pytester = request.getfixturevalue("pytester") assert "PYTEST_ADDOPTS" not in os.environ - pytester._finalize() - assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused" - monkeypatch.undo() - assert os.environ.get("PYTEST_ADDOPTS") == orig def test_run_stdin(pytester: Pytester) -> None: @@ -722,15 +705,13 @@ def test_spawn_uses_tmphome(pytester: Pytester) -> None: pytester._monkeypatch.setenv("CUSTOMENV", "42") p1 = pytester.makepyfile( - """ + f""" import os def test(): assert os.environ["HOME"] == {tmphome!r} assert os.environ["CUSTOMENV"] == "42" - """.format( - tmphome=tmphome - ) + """ ) child = pytester.spawn_pytest(str(p1)) out = child.read() @@ -744,7 +725,7 @@ def test_run_result_repr() -> None: # known exit code r = pytester_mod.RunResult(1, outlines, errlines, duration=0.5) assert repr(r) == ( - f"<RunResult ret={str(pytest.ExitCode.TESTS_FAILED)} len(stdout.lines)=3" + f"<RunResult ret={pytest.ExitCode.TESTS_FAILED!s} len(stdout.lines)=3" " len(stderr.lines)=4 duration=0.50s>" ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_python_path.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_python_path.py index 5ee0f55e36a..73a8725680f 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_python_path.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_python_path.py @@ -1,11 +1,12 @@ +# mypy: allow-untyped-defs import sys from textwrap import dedent from typing import Generator from typing import List from typing import Optional -import pytest from _pytest.pytester import Pytester +import pytest @pytest.fixture() @@ -82,11 +83,11 @@ def test_no_ini(pytester: Pytester, file_structure) -> None: def test_clean_up(pytester: Pytester) -> None: """Test that the plugin cleans up after itself.""" - # This is tough to test behaviorly because the cleanup really runs last. + # This is tough to test behaviorally because the cleanup really runs last. # So the test make several implementation assumptions: # - Cleanup is done in pytest_unconfigure(). - # - Not a hookwrapper. - # So we can add a hookwrapper ourselves to test what it does. + # - Not a hook wrapper. + # So we can add a hook wrapper ourselves to test what it does. pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n") pytester.makepyfile(test_foo="""def test_foo(): pass""") @@ -94,12 +95,14 @@ def test_clean_up(pytester: Pytester) -> None: after: Optional[List[str]] = None class Plugin: - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_unconfigure(self) -> Generator[None, None, None]: nonlocal before, after before = sys.path.copy() - yield - after = sys.path.copy() + try: + return (yield) + finally: + after = sys.path.copy() result = pytester.runpytest_inprocess(plugins=[Plugin()]) assert result.ret == 0 diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_recwarn.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_recwarn.py index d3f218f1660..27ee9aa72f0 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_recwarn.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_recwarn.py @@ -1,10 +1,15 @@ -import re -import warnings +# mypy: allow-untyped-defs +import sys +from typing import List from typing import Optional +from typing import Type +from typing import Union +import warnings import pytest -from _pytest.pytester import Pytester -from _pytest.recwarn import WarningsRecorder +from pytest import ExitCode +from pytest import Pytester +from pytest import WarningsRecorder def test_recwarn_stacklevel(recwarn: WarningsRecorder) -> None: @@ -38,6 +43,47 @@ def test_recwarn_captures_deprecation_warning(recwarn: WarningsRecorder) -> None assert recwarn.pop(DeprecationWarning) +class TestSubclassWarningPop: + class ParentWarning(Warning): + pass + + class ChildWarning(ParentWarning): + pass + + class ChildOfChildWarning(ChildWarning): + pass + + @staticmethod + def raise_warnings_from_list(_warnings: List[Type[Warning]]): + for warn in _warnings: + warnings.warn(f"Warning {warn().__repr__()}", warn) + + def test_pop_finds_exact_match(self): + with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: + self.raise_warnings_from_list( + [self.ChildWarning, self.ParentWarning, self.ChildOfChildWarning] + ) + + assert len(record) == 3 + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ParentWarning + + def test_pop_raises_if_no_match(self): + with pytest.raises(AssertionError): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list([self.ParentWarning]) + record.pop(self.ChildOfChildWarning) + + def test_pop_finds_best_inexact_match(self): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list( + [self.ChildOfChildWarning, self.ChildWarning, self.ChildOfChildWarning] + ) + + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ChildWarning + + class TestWarningsRecorderChecker: def test_recording(self) -> None: rec = WarningsRecorder(_ispytest=True) @@ -114,13 +160,13 @@ class TestDeprecatedCall: # Type ignored because `onceregistry` and `filters` are not # documented API. onceregistry = warnings.onceregistry.copy() # type: ignore - filters = warnings.filters[:] # type: ignore + filters = warnings.filters[:] warn = warnings.warn warn_explicit = warnings.warn_explicit self.test_deprecated_call_raises() self.test_deprecated_call() assert onceregistry == warnings.onceregistry # type: ignore - assert filters == warnings.filters # type: ignore + assert filters == warnings.filters assert warn is warnings.warn assert warn_explicit is warnings.warn_explicit @@ -150,7 +196,7 @@ class TestDeprecatedCall: f() @pytest.mark.parametrize( - "warning_type", [PendingDeprecationWarning, DeprecationWarning] + "warning_type", [PendingDeprecationWarning, DeprecationWarning, FutureWarning] ) @pytest.mark.parametrize("mode", ["context_manager", "call"]) @pytest.mark.parametrize("call_f_first", [True, False]) @@ -173,50 +219,35 @@ class TestDeprecatedCall: with pytest.deprecated_call(): assert f() == 10 - @pytest.mark.parametrize("mode", ["context_manager", "call"]) - def test_deprecated_call_exception_is_raised(self, mode) -> None: - """If the block of the code being tested by deprecated_call() raises an exception, - it must raise the exception undisturbed. - """ - - def f(): - raise ValueError("some exception") - - with pytest.raises(ValueError, match="some exception"): - if mode == "call": - pytest.deprecated_call(f) - else: - with pytest.deprecated_call(): - f() - def test_deprecated_call_specificity(self) -> None: other_warnings = [ Warning, UserWarning, SyntaxWarning, RuntimeWarning, - FutureWarning, ImportWarning, UnicodeWarning, ] for warning in other_warnings: def f(): - warnings.warn(warning("hi")) + warnings.warn(warning("hi")) # noqa: B023 - with pytest.raises(pytest.fail.Exception): - pytest.deprecated_call(f) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(): - f() + with pytest.warns(warning): + with pytest.raises(pytest.fail.Exception): + pytest.deprecated_call(f) + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(): + f() def test_deprecated_call_supports_match(self) -> None: with pytest.deprecated_call(match=r"must be \d+$"): warnings.warn("value must be 42", DeprecationWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(match=r"must be \d+$"): - warnings.warn("this is not here", DeprecationWarning) + with pytest.deprecated_call(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) class TestWarns: @@ -228,8 +259,9 @@ class TestWarns: def test_several_messages(self) -> None: # different messages, b/c Python suppresses multiple identical warnings pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning)) - with pytest.raises(pytest.fail.Exception): - pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) + with pytest.warns(RuntimeWarning): + with pytest.raises(pytest.fail.Exception): + pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning)) def test_function(self) -> None: @@ -244,13 +276,14 @@ class TestWarns: pytest.warns( (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning) ) - pytest.raises( - pytest.fail.Exception, - lambda: pytest.warns( - (RuntimeWarning, SyntaxWarning), - lambda: warnings.warn("w3", UserWarning), - ), - ) + with pytest.warns(): + pytest.raises( + pytest.fail.Exception, + lambda: pytest.warns( + (RuntimeWarning, SyntaxWarning), + lambda: warnings.warn("w3", UserWarning), + ), + ) def test_as_contextmanager(self) -> None: with pytest.warns(RuntimeWarning): @@ -259,48 +292,47 @@ class TestWarns: with pytest.warns(UserWarning): warnings.warn("user", UserWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(RuntimeWarning): - warnings.warn("user", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(RuntimeWarning): + warnings.warn("user", UserWarning) excinfo.match( - r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted. " - r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." + r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n" + r" Emitted warnings: \[UserWarning\('user',?\)\]." ) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(UserWarning): - warnings.warn("runtime", RuntimeWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(UserWarning): + warnings.warn("runtime", RuntimeWarning) excinfo.match( - r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. " - r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\]." + r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" + r" Emitted warnings: \[RuntimeWarning\('runtime',?\)]." ) with pytest.raises(pytest.fail.Exception) as excinfo: with pytest.warns(UserWarning): pass excinfo.match( - r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted. " - r"The list of emitted warnings is: \[\]." + r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" + r" Emitted warnings: \[\]." ) warning_classes = (UserWarning, FutureWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(warning_classes) as warninfo: - warnings.warn("runtime", RuntimeWarning) - warnings.warn("import", ImportWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(warning_classes) as warninfo: + warnings.warn("runtime", RuntimeWarning) + warnings.warn("import", ImportWarning) - message_template = ( - "DID NOT WARN. No warnings of type {0} were emitted. " - "The list of emitted warnings is: {1}." - ) - excinfo.match( - re.escape( - message_template.format( - warning_classes, [each.message for each in warninfo] - ) - ) + messages = [each.message for each in warninfo] + expected_str = ( + f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n" + f" Emitted warnings: {messages}." ) + assert str(excinfo.value) == expected_str + def test_record(self) -> None: with pytest.warns(UserWarning) as record: warnings.warn("user", UserWarning) @@ -317,17 +349,9 @@ class TestWarns: assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" - def test_record_only_none_deprecated_warn(self) -> None: - # This should become an error when WARNS_NONE_ARG is removed in Pytest 8.0 - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - with pytest.warns(None) as record: # type: ignore[call-overload] - warnings.warn("user", UserWarning) - warnings.warn("runtime", RuntimeWarning) - - assert len(record) == 2 - assert str(record[0].message) == "user" - assert str(record[1].message) == "runtime" + def test_record_only_none_type_error(self) -> None: + with pytest.raises(TypeError): + pytest.warns(None) # type: ignore[call-overload] def test_record_by_subclass(self) -> None: with pytest.warns(Warning) as record: @@ -372,25 +396,31 @@ class TestWarns: with pytest.warns(UserWarning, match=r"must be \d+$"): warnings.warn("value must be 42", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(UserWarning, match=r"must be \d+$"): - warnings.warn("this is not here", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r"must be \d+$"): + warnings.warn("this is not here", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(FutureWarning, match=r"must be \d+$"): - warnings.warn("value must be 42", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(FutureWarning, match=r"must be \d+$"): + warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("cccccccccc", UserWarning) - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("aaaaaaaaaa", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(UserWarning, match=r"aaa"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception): - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("cccccccccc", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("cccccccccc", UserWarning) @pytest.mark.filterwarnings("ignore") def test_can_capture_previously_warned(self) -> None: @@ -408,3 +438,160 @@ class TestWarns: with pytest.warns(UserWarning, foo="bar"): # type: ignore pass assert "Unexpected keyword arguments" in str(excinfo.value) + + def test_re_emit_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_multiple(self) -> None: + with pytest.warns(UserWarning): + warnings.warn("first warning", UserWarning) + warnings.warn("second warning", UserWarning) + + def test_re_emit_match_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_match_multiple(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") # if anything is re-emitted + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("first user warning", UserWarning) + warnings.warn("second user warning", UserWarning) + + def test_re_emit_non_match_single(self) -> None: + with pytest.warns(UserWarning, match="v2 warning"): + with pytest.warns(UserWarning, match="v1 warning"): + warnings.warn("v1 warning", UserWarning) + warnings.warn("non-matching v2 warning", UserWarning) + + def test_catch_warning_within_raise(self) -> None: + # warns-in-raises works since https://github.com/pytest-dev/pytest/pull/11129 + with pytest.raises(ValueError, match="some exception"): + with pytest.warns(FutureWarning, match="some warning"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") + # and raises-in-warns has always worked but we'll check for symmetry. + with pytest.warns(FutureWarning, match="some warning"): + with pytest.raises(ValueError, match="some exception"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") + + def test_skip_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.skip("this is OK") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(skipped=1) + + def test_fail_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.fail("BOOM") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + result.assert_outcomes(failed=1) + assert "DID NOT WARN" not in str(result.stdout) + + def test_exit_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + pytest.exit() + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.INTERRUPTED + result.assert_outcomes() + + def test_keyboard_interrupt_within_warns(self, pytester: Pytester) -> None: + """Regression test for #11907.""" + pytester.makepyfile( + """ + import pytest + + def test_it(): + with pytest.warns(Warning): + raise KeyboardInterrupt() + """, + ) + + result = pytester.runpytest_subprocess() + assert result.ret == ExitCode.INTERRUPTED + result.assert_outcomes() + + +def test_raise_type_error_on_invalid_warning() -> None: + """Check pytest.warns validates warning messages are strings (#10865) or + Warning instances (#11959).""" + with pytest.raises(TypeError, match="Warning must be str or Warning"): + with pytest.warns(UserWarning): + warnings.warn(1) # type: ignore + + +@pytest.mark.parametrize( + "message", + [ + pytest.param("Warning", id="str"), + pytest.param(UserWarning(), id="UserWarning"), + pytest.param(Warning(), id="Warning"), + ], +) +def test_no_raise_type_error_on_valid_warning(message: Union[str, Warning]) -> None: + """Check pytest.warns validates warning messages are strings (#10865) or + Warning instances (#11959).""" + with pytest.warns(Warning): + warnings.warn(message) + + +@pytest.mark.skipif( + hasattr(sys, "pypy_version_info"), + reason="Not for pypy", +) +def test_raise_type_error_on_invalid_warning_message_cpython() -> None: + # Check that we get the same behavior with the stdlib, at least if filtering + # (see https://github.com/python/cpython/issues/103577 for details) + with pytest.raises(TypeError): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", "test") + warnings.warn(1) # type: ignore + + +def test_multiple_arg_custom_warning() -> None: + """Test for issue #11906.""" + + class CustomWarning(UserWarning): + def __init__(self, a, b): + pass + + with pytest.warns(CustomWarning): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(CustomWarning, match="not gonna match"): + a, b = 1, 2 + warnings.warn(CustomWarning(a, b)) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_reports.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_reports.py index 31b6cf1afc6..c6baeebc9dd 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_reports.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_reports.py @@ -1,13 +1,15 @@ +# mypy: allow-untyped-defs from typing import Sequence from typing import Union -import pytest from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionRepr from _pytest.config import Config from _pytest.pytester import Pytester +from _pytest.python_api import approx from _pytest.reports import CollectReport from _pytest.reports import TestReport +import pytest class TestReportSerialization: @@ -277,7 +279,7 @@ class TestReportSerialization: ) -> None: """Check serialization/deserialization of report objects containing chained exceptions (#5786)""" pytester.makepyfile( - """ + f""" def foo(): raise ValueError('value error') def test_a(): @@ -285,27 +287,25 @@ class TestReportSerialization: foo() except ValueError as e: raise RuntimeError('runtime error') from e - if {error_during_import}: + if {report_class is CollectReport}: test_a() - """.format( - error_during_import=report_class is CollectReport - ) + """ ) reprec = pytester.inline_run() if report_class is TestReport: - reports: Union[ - Sequence[TestReport], Sequence[CollectReport] - ] = reprec.getreports("pytest_runtest_logreport") + reports: Union[Sequence[TestReport], Sequence[CollectReport]] = ( + reprec.getreports("pytest_runtest_logreport") + ) # we have 3 reports: setup/call/teardown assert len(reports) == 3 # get the call report report = reports[1] else: assert report_class is CollectReport - # two collection reports: session and test file + # three collection reports: session, test file, directory reports = reprec.getreports("pytest_collectreport") - assert len(reports) == 2 + assert len(reports) == 3 report = reports[1] def check_longrepr(longrepr: ExceptionChainRepr) -> None: @@ -409,12 +409,32 @@ class TestReportSerialization: ) -> None: sub_dir = pytester.path.joinpath("ns") sub_dir.mkdir() - sub_dir.joinpath("conftest.py").write_text("import unknown") + sub_dir.joinpath("conftest.py").write_text("import unknown", encoding="utf-8") result = pytester.runpytest_subprocess(".") result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"]) result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*") + def test_report_timestamps_match_duration(self, pytester: Pytester, mock_timing): + reprec = pytester.inline_runsource( + """ + import pytest + from _pytest import timing + @pytest.fixture + def fixture_(): + timing.sleep(5) + yield + timing.sleep(5) + def test_1(fixture_): timing.sleep(10) + """ + ) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 3 + for report in reports: + data = report._to_json() + loaded_report = TestReport._from_json(data) + assert loaded_report.stop - loaded_report.start == approx(report.duration) + class TestHooks: """Test that the hooks are working correctly for plugins""" @@ -450,7 +470,7 @@ class TestHooks: ) reprec = pytester.inline_run() reports = reprec.getreports("pytest_collectreport") - assert len(reports) == 2 + assert len(reports) == 3 for rep in reports: data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_runner.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_runner.py index 2e2c462d978..99c11a3d92c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_runner.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_runner.py @@ -1,14 +1,16 @@ +# mypy: allow-untyped-defs +from functools import partial import inspect import os +from pathlib import Path import sys import types -from pathlib import Path from typing import Dict from typing import List from typing import Tuple from typing import Type +import warnings -import pytest from _pytest import outcomes from _pytest import reports from _pytest import runner @@ -18,6 +20,11 @@ from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.outcomes import OutcomeException from _pytest.pytester import Pytester +import pytest + + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup class TestSetupState: @@ -77,8 +84,6 @@ class TestSetupState: assert r == ["fin3", "fin1"] def test_teardown_multiple_fail(self, pytester: Pytester) -> None: - # Ensure the first exception is the one which is re-raised. - # Ideally both would be reported however. def fin1(): raise Exception("oops1") @@ -90,9 +95,14 @@ class TestSetupState: ss.setup(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) - with pytest.raises(Exception) as err: + with pytest.raises(ExceptionGroup) as err: ss.teardown_exact(None) - assert err.value.args == ("oops2",) + + # Note that finalizers are run LIFO, but because FIFO is more intuitive for + # users we reverse the order of messages, and see the error from fin1 first. + err1, err2 = err.value.exceptions + assert err1.args == ("oops1",) + assert err2.args == ("oops2",) def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None: module_teardown = [] @@ -113,6 +123,25 @@ class TestSetupState: ss.teardown_exact(None) assert module_teardown == ["fin_module"] + def test_teardown_multiple_scopes_several_fail(self, pytester) -> None: + def raiser(exc): + raise exc + + item = pytester.getitem("def test_func(): pass") + mod = item.listchain()[-2] + ss = item.session._setupstate + ss.setup(item) + ss.addfinalizer(partial(raiser, KeyError("from module scope")), mod) + ss.addfinalizer(partial(raiser, TypeError("from function scope 1")), item) + ss.addfinalizer(partial(raiser, ValueError("from function scope 2")), item) + + with pytest.raises(ExceptionGroup, match="errors during test teardown") as e: + ss.teardown_exact(None) + mod, func = e.value.exceptions + assert isinstance(mod, KeyError) + assert isinstance(func.exceptions[0], TypeError) # type: ignore + assert isinstance(func.exceptions[1], ValueError) # type: ignore + class BaseFunctionalTests: def test_passfunction(self, pytester: Pytester) -> None: @@ -380,7 +409,7 @@ class BaseFunctionalTests: # assert rep.outcome.when == "setup" # assert rep.outcome.where.lineno == 3 # assert rep.outcome.where.path.basename == "test_func.py" - # assert instanace(rep.failed.failurerepr, PythonFailureRepr) + # assert isinstance(rep.failed.failurerepr, PythonFailureRepr) def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None: try: @@ -447,6 +476,7 @@ class TestSessionReports: assert not rep.skipped assert rep.passed locinfo = rep.location + assert locinfo is not None assert locinfo[0] == col.path.name assert not locinfo[1] assert locinfo[2] == col.path.name @@ -515,10 +545,10 @@ def test_runtest_in_module_ordering(pytester: Pytester) -> None: @pytest.fixture def mylist(self, request): return request.function.mylist - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(self, item): try: - (yield).get_result() + yield except ValueError: pass def test_hello1(self, mylist): @@ -733,6 +763,73 @@ def test_importorskip_imports_last_module_part() -> None: assert os.path == ospath +class TestImportOrSkipExcType: + """Tests for #11523.""" + + def test_no_warning(self) -> None: + # An attempt on a module which does not exist will raise ModuleNotFoundError, so it will + # be skipped normally and no warning will be issued. + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + + with pytest.raises(pytest.skip.Exception): + pytest.importorskip("TestImportOrSkipExcType_test_no_warning") + + assert captured == [] + + def test_import_error_with_warning(self, pytester: Pytester) -> None: + # Create a module which exists and can be imported, however it raises + # ImportError due to some other problem. In this case we will issue a warning + # about the future behavior change. + fn = pytester.makepyfile("raise ImportError('some specific problem')") + pytester.syspathinsert() + + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + + with pytest.raises(pytest.skip.Exception): + pytest.importorskip(fn.stem) + + [warning] = captured + assert warning.category is pytest.PytestDeprecationWarning + + def test_import_error_suppress_warning(self, pytester: Pytester) -> None: + # Same as test_import_error_with_warning, but we can suppress the warning + # by passing ImportError as exc_type. + fn = pytester.makepyfile("raise ImportError('some specific problem')") + pytester.syspathinsert() + + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + + with pytest.raises(pytest.skip.Exception): + pytest.importorskip(fn.stem, exc_type=ImportError) + + assert captured == [] + + def test_warning_integration(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + def test_foo(): + pytest.importorskip("warning_integration_module") + """ + ) + pytester.makepyfile( + warning_integration_module=""" + raise ImportError("required library foobar not compiled properly") + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*Module 'warning_integration_module' was found, but when imported by pytest it raised:", + "* ImportError('required library foobar not compiled properly')", + "*1 skipped, 1 warning*", + ] + ) + + def test_importorskip_dev_module(monkeypatch) -> None: try: mod = types.ModuleType("mockmodule") @@ -799,12 +896,12 @@ def test_unicode_in_longrepr(pytester: Pytester) -> None: pytester.makeconftest( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(): - outcome = yield - rep = outcome.get_result() + rep = yield if rep.when == "call": rep.longrepr = 'ä' + return rep """ ) pytester.makepyfile( @@ -880,6 +977,7 @@ def test_makereport_getsource_dynamic_code( def test_store_except_info_on_error() -> None: """Test that upon test failure, the exception info is stored on sys.last_traceback and friends.""" + # Simulate item that might raise a specific exception, depending on `raise_error` class var class ItemMightRaise: nodeid = "item_that_raises" @@ -896,6 +994,9 @@ def test_store_except_info_on_error() -> None: # Check that exception info is stored on sys assert sys.last_type is IndexError assert isinstance(sys.last_value, IndexError) + if sys.version_info >= (3, 12, 0): + assert isinstance(sys.last_exc, IndexError) # type: ignore[attr-defined] + assert sys.last_value.args[0] == "TEST" assert sys.last_traceback @@ -904,6 +1005,8 @@ def test_store_except_info_on_error() -> None: runner.pytest_runtest_call(ItemMightRaise()) # type: ignore[arg-type] assert not hasattr(sys, "last_type") assert not hasattr(sys, "last_value") + if sys.version_info >= (3, 12, 0): + assert not hasattr(sys, "last_exc") assert not hasattr(sys, "last_traceback") @@ -978,7 +1081,7 @@ class TestReportContents: ) rec = pytester.inline_run() calls = rec.getcalls("pytest_collectreport") - _, call = calls + _, call, _ = calls assert isinstance(call.report.longrepr, tuple) assert "Skipped" in call.report.longreprtext @@ -1059,3 +1162,20 @@ def test_outcome_exception_bad_msg() -> None: with pytest.raises(TypeError) as excinfo: OutcomeException(func) # type: ignore assert str(excinfo.value) == expected + + +def test_pytest_version_env_var(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("PYTEST_VERSION", "old version") + pytester.makepyfile( + """ + import pytest + import os + + + def test(): + assert os.environ.get("PYTEST_VERSION") == pytest.__version__ + """ + ) + result = pytester.runpytest_inprocess() + assert result.ret == ExitCode.OK + assert os.environ["PYTEST_VERSION"] == "old version" diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_runner_xunit.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_runner_xunit.py index e077ac41e2c..587c9eb9fef 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_runner_xunit.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_runner_xunit.py @@ -1,8 +1,10 @@ +# mypy: allow-untyped-defs """Test correct setup/teardowns at module, class, and instance level.""" + from typing import List -import pytest from _pytest.pytester import Pytester +import pytest def test_module_and_function_setup(pytester: Pytester) -> None: @@ -254,7 +256,7 @@ def test_setup_teardown_function_level_with_optional_argument( sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False ) p = pytester.makepyfile( - """ + f""" import pytest import sys @@ -275,9 +277,7 @@ def test_setup_teardown_function_level_with_optional_argument( def test_method_1(self): pass def test_method_2(self): pass - """.format( - arg=arg - ) + """ ) result = pytester.inline_run(p) result.assertoutcome(passed=4) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_scope.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_scope.py index 09ee1343a80..1727c2ee1bb 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_scope.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_scope.py @@ -1,7 +1,7 @@ import re -import pytest from _pytest.scope import Scope +import pytest def test_ordering() -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_session.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_session.py index 3ca6d390383..8624af478b1 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_session.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_session.py @@ -1,7 +1,8 @@ -import pytest +# mypy: allow-untyped-defs from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest class SessionTests: @@ -172,8 +173,9 @@ class SessionTests: except pytest.skip.Exception: # pragma: no cover pytest.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") - assert len(reports) == 1 - assert reports[0].skipped + # Session, Dir + assert len(reports) == 2 + assert reports[1].skipped class TestNewSession(SessionTests): @@ -265,9 +267,9 @@ def test_plugin_already_exists(pytester: Pytester) -> None: def test_exclude(pytester: Pytester) -> None: hellodir = pytester.mkdir("hello") - hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror", encoding="utf-8") hello2dir = pytester.mkdir("hello2") - hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror", encoding="utf-8") pytester.makepyfile(test_ok="def test_pass(): pass") result = pytester.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 @@ -276,13 +278,13 @@ def test_exclude(pytester: Pytester) -> None: def test_exclude_glob(pytester: Pytester) -> None: hellodir = pytester.mkdir("hello") - hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror", encoding="utf-8") hello2dir = pytester.mkdir("hello2") - hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror", encoding="utf-8") hello3dir = pytester.mkdir("hallo3") - hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror") + hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror", encoding="utf-8") subdir = pytester.mkdir("sub") - subdir.joinpath("test_hello4.py").write_text("x y syntaxerror") + subdir.joinpath("test_hello4.py").write_text("x y syntaxerror", encoding="utf-8") pytester.makepyfile(test_ok="def test_pass(): pass") result = pytester.runpytest("--ignore-glob=*h[ea]llo*") assert result.ret == 0 @@ -335,6 +337,56 @@ def test_sessionfinish_with_start(pytester: Pytester) -> None: assert res.ret == ExitCode.NO_TESTS_COLLECTED +def test_collection_args_do_not_duplicate_modules(pytester: Pytester) -> None: + """Test that when multiple collection args are specified on the command line + for the same module, only a single Module collector is created. + + Regression test for #723, #3358. + """ + pytester.makepyfile( + **{ + "d/test_it": """ + def test_1(): pass + def test_2(): pass + """ + } + ) + + result = pytester.runpytest( + "--collect-only", + "d/test_it.py::test_1", + "d/test_it.py::test_2", + ) + result.stdout.fnmatch_lines( + [ + " <Dir d>", + " <Module test_it.py>", + " <Function test_1>", + " <Function test_2>", + ], + consecutive=True, + ) + + # Different, but related case. + result = pytester.runpytest( + "--collect-only", + "--keep-duplicates", + "d", + "d", + ) + result.stdout.fnmatch_lines( + [ + " <Dir d>", + " <Module test_it.py>", + " <Function test_1>", + " <Function test_2>", + " <Function test_1>", + " <Function test_2>", + ], + consecutive=True, + ) + + @pytest.mark.parametrize("path", ["root", "{relative}/root", "{environment}/root"]) def test_rootdir_option_arg( pytester: Pytester, monkeypatch: MonkeyPatch, path: str @@ -367,3 +419,63 @@ def test_rootdir_wrong_option_arg(pytester: Pytester) -> None: result.stderr.fnmatch_lines( ["*Directory *wrong_dir* not found. Check your '--rootdir' option.*"] ) + + +def test_shouldfail_is_sticky(pytester: Pytester) -> None: + """Test that session.shouldfail cannot be reset to False after being set. + + Issue #11706. + """ + pytester.makeconftest( + """ + def pytest_sessionfinish(session): + assert session.shouldfail + session.shouldfail = False + assert session.shouldfail + """ + ) + pytester.makepyfile( + """ + import pytest + + def test_foo(): + pytest.fail("This is a failing test") + + def test_bar(): pass + """ + ) + + result = pytester.runpytest("--maxfail=1", "-Wall") + + result.assert_outcomes(failed=1, warnings=1) + result.stdout.fnmatch_lines("*session.shouldfail cannot be unset*") + + +def test_shouldstop_is_sticky(pytester: Pytester) -> None: + """Test that session.shouldstop cannot be reset to False after being set. + + Issue #11706. + """ + pytester.makeconftest( + """ + def pytest_sessionfinish(session): + assert session.shouldstop + session.shouldstop = False + assert session.shouldstop + """ + ) + pytester.makepyfile( + """ + import pytest + + def test_foo(): + pytest.fail("This is a failing test") + + def test_bar(): pass + """ + ) + + result = pytester.runpytest("--stepwise", "-Wall") + + result.assert_outcomes(failed=1, warnings=1) + result.stdout.fnmatch_lines("*session.shouldstop cannot be unset*") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_setuponly.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_setuponly.py index fe4bdc514eb..8638f5a6140 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_setuponly.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_setuponly.py @@ -1,8 +1,9 @@ +# mypy: allow-untyped-defs import sys -import pytest from _pytest.config import ExitCode from _pytest.pytester import Pytester +import pytest @pytest.fixture(params=["--setup-only", "--setup-plan", "--setup-show"], scope="module") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_skipping.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_skipping.py index 3010943607c..a1511b26d1c 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_skipping.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_skipping.py @@ -1,12 +1,13 @@ +# mypy: allow-untyped-defs import sys import textwrap -import pytest from _pytest.pytester import Pytester from _pytest.runner import runtestprotocol from _pytest.skipping import evaluate_skip_marks from _pytest.skipping import evaluate_xfail_marks from _pytest.skipping import pytest_runtest_setup +import pytest class TestEvaluation: @@ -73,16 +74,15 @@ class TestEvaluation: """@pytest.mark.skipif("not hasattr(os, 'murks')")""", """@pytest.mark.skipif(condition="hasattr(os, 'murks')")""", ] - for i in range(0, 2): + for i in range(2): item = pytester.getitem( - """ + f""" import pytest - %s - %s + {lines[i]} + {lines[(i + 1) % 2]} def test_func(): pass """ - % (lines[i], lines[(i + 1) % 2]) ) skipped = evaluate_skip_marks(item) assert skipped @@ -195,7 +195,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "root"} """ - ) + ), + encoding="utf-8", ) root.joinpath("test_root.py").write_text( textwrap.dedent( @@ -206,7 +207,8 @@ class TestEvaluation: def test_root(): assert False """ - ) + ), + encoding="utf-8", ) foo = root.joinpath("foo") foo.mkdir() @@ -219,7 +221,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "foo"} """ - ) + ), + encoding="utf-8", ) foo.joinpath("test_foo.py").write_text( textwrap.dedent( @@ -230,7 +233,8 @@ class TestEvaluation: def test_foo(): assert False """ - ) + ), + encoding="utf-8", ) bar = root.joinpath("bar") bar.mkdir() @@ -243,7 +247,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "bar"} """ - ) + ), + encoding="utf-8", ) bar.joinpath("test_bar.py").write_text( textwrap.dedent( @@ -254,7 +259,8 @@ class TestEvaluation: def test_bar(): assert False """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run("-vs", "--capture=no") @@ -441,10 +447,8 @@ class TestXFail: result = pytester.runpytest(p, "-rx") result.stdout.fnmatch_lines( [ - "*test_one*test_this*", - "*NOTRUN*noway", - "*test_one*test_this_true*", - "*NOTRUN*condition:*True*", + "*test_one*test_this - reason: *NOTRUN* noway", + "*test_one*test_this_true - reason: *NOTRUN* condition: True", "*1 passed*", ] ) @@ -461,9 +465,7 @@ class TestXFail: """ ) result = pytester.runpytest(p, "-rx") - result.stdout.fnmatch_lines( - ["*test_one*test_this*", "*NOTRUN*hello", "*1 xfailed*"] - ) + result.stdout.fnmatch_lines(["*test_one*test_this*NOTRUN*hello", "*1 xfailed*"]) def test_xfail_xpass(self, pytester: Pytester) -> None: p = pytester.makepyfile( @@ -489,7 +491,7 @@ class TestXFail: result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) result = pytester.runpytest(p, "-rx") - result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*reason:*hello*"]) + result.stdout.fnmatch_lines(["*XFAIL*test_this*reason:*hello*"]) result = pytester.runpytest(p, "--runxfail") result.stdout.fnmatch_lines(["*1 pass*"]) @@ -507,7 +509,7 @@ class TestXFail: result = pytester.runpytest(p) result.stdout.fnmatch_lines(["*1 xfailed*"]) result = pytester.runpytest(p, "-rx") - result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*reason:*hello*"]) + result.stdout.fnmatch_lines(["*XFAIL*test_this*reason:*hello*"]) result = pytester.runpytest(p, "--runxfail") result.stdout.fnmatch_lines( """ @@ -543,7 +545,7 @@ class TestXFail: """ ) result = pytester.runpytest(p, "-rxX") - result.stdout.fnmatch_lines(["*XFAIL*test_this*", "*NOTRUN*"]) + result.stdout.fnmatch_lines(["*XFAIL*test_this*NOTRUN*"]) def test_dynamic_xfail_set_during_funcarg_setup(self, pytester: Pytester) -> None: p = pytester.makepyfile( @@ -604,7 +606,7 @@ class TestXFail: @pytest.mark.xfail(raises=%s) def test_raises(): raise %s() - """ + """ # noqa: UP031 (python syntax issues) % (expected, actual) ) result = pytester.runpytest(p) @@ -622,7 +624,7 @@ class TestXFail: """ ) result = pytester.runpytest(p, "-rxX") - result.stdout.fnmatch_lines(["*XFAIL*", "*unsupported feature*"]) + result.stdout.fnmatch_lines(["*XFAIL*unsupported feature*"]) assert result.ret == 0 @pytest.mark.parametrize("strict", [True, False]) @@ -633,7 +635,8 @@ class TestXFail: @pytest.mark.xfail(reason='unsupported feature', strict=%s) def test_foo(): - with open('foo_executed', 'w'): pass # make sure test executes + with open('foo_executed', 'w', encoding='utf-8'): + pass # make sure test executes """ % strict ) @@ -646,7 +649,7 @@ class TestXFail: result.stdout.fnmatch_lines( [ "*test_strict_xfail*", - "XPASS test_strict_xfail.py::test_foo unsupported feature", + "XPASS test_strict_xfail.py::test_foo - unsupported feature", ] ) assert result.ret == (1 if strict else 0) @@ -905,7 +908,7 @@ class TestSkipif: @pytest.mark.skipif(%(params)s) def test_that(): assert 0 - """ + """ # noqa: UP031 (python syntax issues) % dict(params=params) ) result = pytester.runpytest(p, "-s", "-rs") @@ -931,15 +934,13 @@ class TestSkipif: self, pytester: Pytester, marker, msg1, msg2 ) -> None: pytester.makepyfile( - test_foo=""" + test_foo=f""" import pytest @pytest.mark.{marker}(False, reason='first_condition') @pytest.mark.{marker}(True, reason='second_condition') def test_foobar(): assert 1 - """.format( - marker=marker - ) + """ ) result = pytester.runpytest("-s", "-rsxX") result.stdout.fnmatch_lines( @@ -986,33 +987,34 @@ def test_skipped_reasons_functional(pytester: Pytester) -> None: pytester.makepyfile( test_one=""" import pytest - from conftest import doskip + from helpers import doskip - def setup_function(func): - doskip() + def setup_function(func): # LINE 4 + doskip("setup function") def test_func(): pass - class TestClass(object): + class TestClass: def test_method(self): - doskip() + doskip("test method") - @pytest.mark.skip("via_decorator") + @pytest.mark.skip("via_decorator") # LINE 14 def test_deco(self): assert 0 """, - conftest=""" + helpers=""" import pytest, sys - def doskip(): + def doskip(reason): assert sys._getframe().f_lineno == 3 - pytest.skip('test') + pytest.skip(reason) # LINE 4 """, ) result = pytester.runpytest("-rs") result.stdout.fnmatch_lines_random( [ - "SKIPPED [[]2[]] conftest.py:4: test", + "SKIPPED [[]1[]] test_one.py:7: setup function", + "SKIPPED [[]1[]] helpers.py:4: test method", "SKIPPED [[]1[]] test_one.py:14: via_decorator", ] ) @@ -1139,14 +1141,12 @@ def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - markline = " ^" + markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info is not None and pypy_version_info < (6,): - markline = markline[5:] - elif sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - markline = markline[4:] + markline = markline[1:] - if sys.version_info[:2] >= (3, 10): + if sys.version_info >= (3, 10): expected = [ "*ERROR*test_nameerror*", "*asd*", @@ -1185,7 +1185,7 @@ def test_xfail_skipif_with_globals(pytester: Pytester) -> None: """ ) result = pytester.runpytest("-rsx") - result.stdout.fnmatch_lines(["*SKIP*x == 3*", "*XFAIL*test_boolean*", "*x == 3*"]) + result.stdout.fnmatch_lines(["*SKIP*x == 3*", "*XFAIL*test_boolean*x == 3*"]) def test_default_markers(pytester: Pytester) -> None: @@ -1297,8 +1297,7 @@ class TestBooleanCondition: result = pytester.runpytest("-rxs") result.stdout.fnmatch_lines( """ - *XFAIL* - *True123* + *XFAIL*True123* *1 xfail* """ ) @@ -1444,6 +1443,27 @@ def test_relpath_rootdir(pytester: Pytester) -> None: ) +def test_skip_from_fixture(pytester: Pytester) -> None: + pytester.makepyfile( + **{ + "tests/test_1.py": """ + import pytest + def test_pass(arg): + pass + @pytest.fixture + def arg(): + condition = True + if condition: + pytest.skip("Fixture conditional skip") + """, + } + ) + result = pytester.runpytest("-rs", "tests/test_1.py", "--rootdir=tests") + result.stdout.fnmatch_lines( + ["SKIPPED [[]1[]] tests/test_1.py:2: Fixture conditional skip"] + ) + + def test_skip_using_reason_works_ok(pytester: Pytester) -> None: p = pytester.makepyfile( """ @@ -1472,54 +1492,6 @@ def test_fail_using_reason_works_ok(pytester: Pytester) -> None: result.assert_outcomes(failed=1) -def test_fail_fails_with_msg_and_reason(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - - def test_fail_both_arguments(): - pytest.fail(reason="foo", msg="bar") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - "*UsageError: Passing both ``reason`` and ``msg`` to pytest.fail(...) is not permitted.*" - ) - result.assert_outcomes(failed=1) - - -def test_skip_fails_with_msg_and_reason(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - - def test_skip_both_arguments(): - pytest.skip(reason="foo", msg="bar") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - "*UsageError: Passing both ``reason`` and ``msg`` to pytest.skip(...) is not permitted.*" - ) - result.assert_outcomes(failed=1) - - -def test_exit_with_msg_and_reason_fails(pytester: Pytester) -> None: - p = pytester.makepyfile( - """ - import pytest - - def test_exit_both_arguments(): - pytest.exit(reason="foo", msg="bar") - """ - ) - result = pytester.runpytest(p) - result.stdout.fnmatch_lines( - "*UsageError: cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`.*" - ) - result.assert_outcomes(failed=1) - - def test_exit_with_reason_works_ok(pytester: Pytester) -> None: p = pytester.makepyfile( """ diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_stash.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_stash.py index 2c9df4832e4..e523c4e6f2b 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_stash.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_stash.py @@ -1,6 +1,6 @@ -import pytest from _pytest.stash import Stash from _pytest.stash import StashKey +import pytest def test_stash() -> None: diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_stepwise.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_stepwise.py index 63d29d6241f..472afea6620 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_stepwise.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_stepwise.py @@ -1,6 +1,11 @@ -import pytest +# mypy: allow-untyped-defs +from pathlib import Path + +from _pytest.cacheprovider import Cache from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +from _pytest.stepwise import STEPWISE_CACHE_DIR +import pytest @pytest.fixture @@ -277,4 +282,77 @@ def test_stepwise_skip_is_independent(pytester: Pytester) -> None: def test_sw_skip_help(pytester: Pytester) -> None: result = pytester.runpytest("-h") - result.stdout.fnmatch_lines("*implicitly enables --stepwise.") + result.stdout.fnmatch_lines("*Implicitly enables --stepwise.") + + +def test_stepwise_xdist_dont_store_lastfailed(pytester: Pytester) -> None: + pytester.makefile( + ext=".ini", + pytest=f"[pytest]\ncache_dir = {pytester.path}\n", + ) + + pytester.makepyfile( + conftest=""" +import pytest + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config) -> None: + config.workerinput = True +""" + ) + pytester.makepyfile( + test_one=""" +def test_one(): + assert False +""" + ) + result = pytester.runpytest("--stepwise") + assert result.ret == pytest.ExitCode.INTERRUPTED + + stepwise_cache_file = ( + pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR + ) + assert not Path(stepwise_cache_file).exists() + + +def test_disabled_stepwise_xdist_dont_clear_cache(pytester: Pytester) -> None: + pytester.makefile( + ext=".ini", + pytest=f"[pytest]\ncache_dir = {pytester.path}\n", + ) + + stepwise_cache_file = ( + pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR + ) + stepwise_cache_dir = stepwise_cache_file.parent + stepwise_cache_dir.mkdir(exist_ok=True, parents=True) + + stepwise_cache_file_relative = f"{Cache._CACHE_PREFIX_VALUES}/{STEPWISE_CACHE_DIR}" + + expected_value = '"test_one.py::test_one"' + content = {f"{stepwise_cache_file_relative}": expected_value} + + pytester.makefile(ext="", **content) + + pytester.makepyfile( + conftest=""" +import pytest + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config) -> None: + config.workerinput = True +""" + ) + pytester.makepyfile( + test_one=""" +def test_one(): + assert True +""" + ) + result = pytester.runpytest() + assert result.ret == 0 + + assert Path(stepwise_cache_file).exists() + with stepwise_cache_file.open(encoding="utf-8") as file_handle: + observed_value = file_handle.readlines() + assert [expected_value] == observed_value diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_terminal.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_terminal.py index 23f597e3325..5ed0fee82e6 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_terminal.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_terminal.py @@ -1,22 +1,22 @@ +# mypy: allow-untyped-defs """Terminal reporting of the full testing process.""" -import collections + +from io import StringIO import os +from pathlib import Path import sys import textwrap -from io import StringIO -from pathlib import Path from types import SimpleNamespace from typing import cast from typing import Dict from typing import List +from typing import NamedTuple from typing import Tuple import pluggy -import _pytest.config -import _pytest.terminal -import pytest from _pytest._io.wcwidth import wcswidth +import _pytest.config from _pytest.config import Config from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch @@ -24,6 +24,7 @@ from _pytest.pytester import Pytester from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport +import _pytest.terminal from _pytest.terminal import _folded_skips from _pytest.terminal import _format_trimmed from _pytest.terminal import _get_line_with_reprcrash_message @@ -31,8 +32,12 @@ from _pytest.terminal import _get_raw_skip_reason from _pytest.terminal import _plugin_nameversions from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter +import pytest + -DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) +class DistInfo(NamedTuple): + project_name: str + version: int TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"}) @@ -155,7 +160,6 @@ class TestTerminal: self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: """Test for "collecting" being updated after 0.5s""" - pytester.makepyfile( **{ "test1.py": """ @@ -244,7 +248,8 @@ class TestTerminal: def test_method(self): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-vv") assert result.ret == 0 @@ -385,21 +390,53 @@ class TestTerminal: def test_10(): pytest.xfail("It's 🕙 o'clock") + + @pytest.mark.skip( + reason="1 cannot do foobar because baz is missing due to I don't know what" + ) + def test_long_skip(): + pass + + @pytest.mark.xfail( + reason="2 cannot do foobar because baz is missing due to I don't know what" + ) + def test_long_xfail(): + print(1 / 0) """ ) + + common_output = [ + "test_verbose_skip_reason.py::test_1 SKIPPED (123) *", + "test_verbose_skip_reason.py::test_2 XPASS (456) *", + "test_verbose_skip_reason.py::test_3 XFAIL (789) *", + "test_verbose_skip_reason.py::test_4 XFAIL *", + "test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *", + "test_verbose_skip_reason.py::test_6 XPASS *", + "test_verbose_skip_reason.py::test_7 SKIPPED *", + "test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *", + "test_verbose_skip_reason.py::test_9 XFAIL *", + "test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *", + ] + result = pytester.runpytest("-v") result.stdout.fnmatch_lines( [ - "test_verbose_skip_reason.py::test_1 SKIPPED (123) *", - "test_verbose_skip_reason.py::test_2 XPASS (456) *", - "test_verbose_skip_reason.py::test_3 XFAIL (789) *", - "test_verbose_skip_reason.py::test_4 XFAIL *", - "test_verbose_skip_reason.py::test_5 SKIPPED (unconditional skip) *", - "test_verbose_skip_reason.py::test_6 XPASS *", - "test_verbose_skip_reason.py::test_7 SKIPPED *", - "test_verbose_skip_reason.py::test_8 SKIPPED (888 is great) *", - "test_verbose_skip_reason.py::test_9 XFAIL *", - "test_verbose_skip_reason.py::test_10 XFAIL (It's 🕙 o'clock) *", + *common_output, + "test_verbose_skip_reason.py::test_long_skip SKIPPED (1 cannot *...) *", + "test_verbose_skip_reason.py::test_long_xfail XFAIL (2 cannot *...) *", + ] + ) + + result = pytester.runpytest("-vv") + result.stdout.fnmatch_lines( + [ + *common_output, + "test_verbose_skip_reason.py::test_long_skip SKIPPED" + " (1 cannot do foobar", + "because baz is missing due to I don't know what) *", + "test_verbose_skip_reason.py::test_long_xfail XFAIL" + " (2 cannot do foobar", + "because baz is missing due to I don't know what) *", ] ) @@ -414,7 +451,11 @@ class TestCollectonly: ) result = pytester.runpytest("--collect-only") result.stdout.fnmatch_lines( - ["<Module test_collectonly_basic.py>", " <Function test_func>"] + [ + "<Dir test_collectonly_basic0>", + " <Module test_collectonly_basic.py>", + " <Function test_func>", + ] ) def test_collectonly_skipped_module(self, pytester: Pytester) -> None: @@ -443,14 +484,15 @@ class TestCollectonly: result = pytester.runpytest("--collect-only", "--verbose") result.stdout.fnmatch_lines( [ - "<YamlFile test1.yaml>", - " <YamlItem test1.yaml>", - "<Module test_collectonly_displays_test_description.py>", - " <Function test_with_description>", - " This test has a description.", - " ", - " more1.", - " more2.", + "<Dir test_collectonly_displays_test_description0>", + " <YamlFile test1.yaml>", + " <YamlItem test1.yaml>", + " <Module test_collectonly_displays_test_description.py>", + " <Function test_with_description>", + " This test has a description.", + " ", + " more1.", + " more2.", ], consecutive=True, ) @@ -682,20 +724,18 @@ class TestTerminalFunctional: pass """ ) - result = pytester.runpytest( - "-Wignore::pytest.PytestRemovedIn7Warning", "-k", "test_two:", testpath - ) + result = pytester.runpytest("-k", "test_t", testpath) result.stdout.fnmatch_lines( ["collected 3 items / 1 deselected / 2 selected", "*test_deselected.py ..*"] ) assert result.ret == 0 - def test_deselected_with_hookwrapper(self, pytester: Pytester) -> None: + def test_deselected_with_hook_wrapper(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(config, items): yield deselected = items.pop() @@ -751,6 +791,33 @@ class TestTerminalFunctional: result.stdout.no_fnmatch_line("*= 1 deselected =*") assert result.ret == 0 + def test_selected_count_with_error(self, pytester: Pytester) -> None: + pytester.makepyfile( + test_selected_count_3=""" + def test_one(): + pass + def test_two(): + pass + def test_three(): + pass + """, + test_selected_count_error=""" + 5/0 + def test_foo(): + pass + def test_bar(): + pass + """, + ) + result = pytester.runpytest("-k", "test_t") + result.stdout.fnmatch_lines( + [ + "collected 3 items / 1 error / 1 deselected / 2 selected", + "* ERROR collecting test_selected_count_error.py *", + ] + ) + assert result.ret == ExitCode.INTERRUPTED + def test_no_skip_summary_if_failure(self, pytester: Pytester) -> None: pytester.makepyfile( """ @@ -801,13 +868,7 @@ class TestTerminalFunctional: result.stdout.fnmatch_lines( [ "*===== test session starts ====*", - "platform %s -- Python %s*pytest-%s**pluggy-%s" - % ( - sys.platform, - verinfo, - pytest.__version__, - pluggy.__version__, - ), + f"platform {sys.platform} -- Python {verinfo}*pytest-{pytest.__version__}**pluggy-{pluggy.__version__}", "*test_header_trailer_info.py .*", "=* 1 passed*in *.[0-9][0-9]s *=", ] @@ -828,13 +889,7 @@ class TestTerminalFunctional: result = pytester.runpytest("--no-header") verinfo = ".".join(map(str, sys.version_info[:3])) result.stdout.no_fnmatch_line( - "platform %s -- Python %s*pytest-%s**pluggy-%s" - % ( - sys.platform, - verinfo, - pytest.__version__, - pluggy.__version__, - ) + f"platform {sys.platform} -- Python {verinfo}*pytest-{pytest.__version__}**pluggy-{pluggy.__version__}" ) if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.no_fnmatch_line("plugins: *") @@ -850,7 +905,7 @@ class TestTerminalFunctional: # with configfile pytester.makeini("""[pytest]""") result = pytester.runpytest() - result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"]) # with testpaths option, and not passing anything in the command-line pytester.makeini( @@ -861,33 +916,31 @@ class TestTerminalFunctional: ) result = pytester.runpytest() result.stdout.fnmatch_lines( - ["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"] + ["rootdir: *test_header0", "configfile: tox.ini", "testpaths: tests, gui"] ) # with testpaths option, passing directory in command-line: do not show testpaths then result = pytester.runpytest("tests") - result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"]) + result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"]) def test_header_absolute_testpath( self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: - """Regresstion test for #7814.""" + """Regression test for #7814.""" tests = pytester.path.joinpath("tests") tests.mkdir() pytester.makepyprojecttoml( - """ + f""" [tool.pytest.ini_options] - testpaths = ['{}'] - """.format( - tests - ) + testpaths = ['{tests}'] + """ ) result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format( - tests - ) + "rootdir: *absolute_testpath0", + "configfile: pyproject.toml", + f"testpaths: {tests}", ] ) @@ -939,6 +992,22 @@ class TestTerminalFunctional: ] ) + def test_noshowlocals_addopts_override(self, pytester: Pytester) -> None: + pytester.makeini("[pytest]\naddopts=--showlocals") + p1 = pytester.makepyfile( + """ + def test_noshowlocals(): + x = 3 + y = "x" * 5000 + assert 0 + """ + ) + + # Override global --showlocals for py.test via arg + result = pytester.runpytest(p1, "--no-showlocals") + result.stdout.no_fnmatch_line("x* = 3") + result.stdout.no_fnmatch_line("y* = 'xxxxxx*") + def test_showlocals_short(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -1080,7 +1149,21 @@ class TestTerminalFunctional: assert result.stdout.lines.count(expected) == 1 -def test_fail_extra_reporting(pytester: Pytester, monkeypatch) -> None: +@pytest.mark.parametrize( + ("use_ci", "expected_message"), + ( + (True, f"- AssertionError: {'this_failed'*100}"), + (False, "- AssertionError: this_failedt..."), + ), + ids=("on CI", "not on CI"), +) +def test_fail_extra_reporting( + pytester: Pytester, monkeypatch, use_ci: bool, expected_message: str +) -> None: + if use_ci: + monkeypatch.setenv("CI", "true") + else: + monkeypatch.delenv("CI", raising=False) monkeypatch.setenv("COLUMNS", "80") pytester.makepyfile("def test_this(): assert 0, 'this_failed' * 100") result = pytester.runpytest("-rN") @@ -1089,7 +1172,7 @@ def test_fail_extra_reporting(pytester: Pytester, monkeypatch) -> None: result.stdout.fnmatch_lines( [ "*test summary*", - "FAILED test_fail_extra_reporting.py::test_this - AssertionError: this_failedt...", + f"FAILED test_fail_extra_reporting.py::test_this {expected_message}", ] ) @@ -1176,14 +1259,14 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None: "=*= FAILURES =*=", "{red}{bold}_*_ test_this _*_{reset}", "", - " {kw}def{hl-reset} {function}test_this{hl-reset}():", - "> fail()", + " {reset}{kw}def{hl-reset} {function}test_this{hl-reset}():{endline}", + "> fail(){endline}", "", "{bold}{red}test_color_yes.py{reset}:5: ", "_ _ * _ _*", "", - " {kw}def{hl-reset} {function}fail{hl-reset}():", - "> {kw}assert{hl-reset} {number}0{hl-reset}", + " {reset}{kw}def{hl-reset} {function}fail{hl-reset}():{endline}", + "> {kw}assert{hl-reset} {number}0{hl-reset}{endline}", "{bold}{red}E assert 0{reset}", "", "{bold}{red}test_color_yes.py{reset}:2: AssertionError", @@ -1203,9 +1286,9 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None: "=*= FAILURES =*=", "{red}{bold}_*_ test_this _*_{reset}", "{bold}{red}test_color_yes.py{reset}:5: in test_this", - " fail()", + " {reset}fail(){endline}", "{bold}{red}test_color_yes.py{reset}:2: in fail", - " {kw}assert{hl-reset} {number}0{hl-reset}", + " {reset}{kw}assert{hl-reset} {number}0{hl-reset}{endline}", "{bold}{red}E assert 0{reset}", "{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}", ] @@ -1450,6 +1533,19 @@ class TestGenericReporting: s = result.stdout.str() assert "def test_func2" not in s + def test_tb_crashline_pytrace_false(self, pytester: Pytester, option) -> None: + p = pytester.makepyfile( + """ + import pytest + def test_func1(): + pytest.fail('test_func1', pytrace=False) + """ + ) + result = pytester.runpytest("--tb=line") + result.stdout.str() + bn = p.name + result.stdout.fnmatch_lines(["*%s:3: Failed: test_func1" % bn]) + def test_pytest_report_header(self, pytester: Pytester, option) -> None: pytester.makeconftest( """ @@ -1463,7 +1559,8 @@ class TestGenericReporting: """ def pytest_report_header(config, start_path): return ["line1", str(start_path)] -""" +""", + encoding="utf-8", ) result = pytester.runpytest("a") result.stdout.fnmatch_lines(["*hello: 42*", "line1", str(pytester.path)]) @@ -1567,7 +1664,7 @@ def test_fdopen_kept_alive_issue124(pytester: Pytester) -> None: import os, sys k = [] def test_open_file_and_keep_alive(capfd): - stdout = os.fdopen(1, 'w', 1) + stdout = os.fdopen(1, 'w', buffering=1, encoding='utf-8') k.append(stdout) def test_close_kept_alive_file(): @@ -1696,7 +1793,7 @@ def test_terminal_no_summary_warnings_header_once(pytester: Pytester) -> None: @pytest.fixture(scope="session") def tr() -> TerminalReporter: - config = _pytest.config._prepareconfig() + config = _pytest.config._prepareconfig([]) return TerminalReporter(config) @@ -1895,9 +1992,9 @@ class TestClassicOutputStyle: result = pytester.runpytest("-o", "console_output_style=classic") result.stdout.fnmatch_lines( [ + f"sub{os.sep}test_three.py .F.", "test_one.py .", "test_two.py F", - f"sub{os.sep}test_three.py .F.", "*2 failed, 3 passed in*", ] ) @@ -1906,18 +2003,18 @@ class TestClassicOutputStyle: result = pytester.runpytest("-o", "console_output_style=classic", "-v") result.stdout.fnmatch_lines( [ - "test_one.py::test_one PASSED", - "test_two.py::test_two FAILED", f"sub{os.sep}test_three.py::test_three_1 PASSED", f"sub{os.sep}test_three.py::test_three_2 FAILED", f"sub{os.sep}test_three.py::test_three_3 PASSED", + "test_one.py::test_one PASSED", + "test_two.py::test_two FAILED", "*2 failed, 3 passed in*", ] ) def test_quiet(self, pytester: Pytester, test_files) -> None: result = pytester.runpytest("-o", "console_output_style=classic", "-q") - result.stdout.fnmatch_lines([".F.F.", "*2 failed, 3 passed in*"]) + result.stdout.fnmatch_lines([".F..F", "*2 failed, 3 passed in*"]) class TestProgressOutputStyle: @@ -2124,6 +2221,24 @@ class TestProgressOutputStyle: output = pytester.runpytest("--capture=no") output.stdout.no_fnmatch_line("*%]*") + def test_capture_no_progress_enabled( + self, many_tests_files, pytester: Pytester + ) -> None: + pytester.makeini( + """ + [pytest] + console_output_style = progress-even-when-capture-no + """ + ) + output = pytester.runpytest("-s") + output.stdout.re_match_lines( + [ + r"test_bar.py \.{10} \s+ \[ 50%\]", + r"test_foo.py \.{5} \s+ \[ 75%\]", + r"test_foobar.py \.{5} \s+ \[100%\]", + ] + ) + class TestProgressWithTeardown: """Ensure we show the correct percentages for tests that fail during teardown (#3088)""" @@ -2260,10 +2375,15 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None: def mock_get_pos(*args): return mocked_pos - monkeypatch.setattr(_pytest.terminal, "_get_pos", mock_get_pos) + monkeypatch.setattr(_pytest.terminal, "_get_node_id_with_markup", mock_get_pos) + + class Namespace: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) class config: - pass + def __init__(self): + self.option = Namespace(verbose=0) class rep: def _get_verbose_word(self, *args): @@ -2274,10 +2394,21 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None: pass def check(msg, width, expected): + class DummyTerminalWriter: + fullwidth = width + + def markup(self, word: str, **markup: str): + return word + __tracebackhide__ = True if msg: rep.longrepr.reprcrash.message = msg # type: ignore - actual = _get_line_with_reprcrash_message(config, rep(), width) # type: ignore + actual = _get_line_with_reprcrash_message( + config(), # type: ignore[arg-type] + rep(), # type: ignore[arg-type] + DummyTerminalWriter(), # type: ignore[arg-type] + {}, + ) assert actual == expected if actual != f"{mocked_verbose_word} {mocked_pos}": @@ -2317,6 +2448,43 @@ def test_line_with_reprcrash(monkeypatch: MonkeyPatch) -> None: check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐") +def test_short_summary_with_verbose( + monkeypatch: MonkeyPatch, pytester: Pytester +) -> None: + """With -vv do not truncate the summary info (#11777).""" + # On CI we also do not truncate the summary info, monkeypatch it to ensure we + # are testing against the -vv flag on CI. + monkeypatch.setattr(_pytest.terminal, "running_on_ci", lambda: False) + + string_length = 200 + pytester.makepyfile( + f""" + def test(): + s1 = "A" * {string_length} + s2 = "B" * {string_length} + assert s1 == s2 + """ + ) + + # No -vv, summary info should be truncated. + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*short test summary info*", + "* assert 'AAA...", + ], + ) + + # No truncation with -vv. + result = pytester.runpytest("-vv") + result.stdout.fnmatch_lines( + [ + "*short test summary info*", + f"*{'A' * string_length}*{'B' * string_length}'", + ] + ) + + @pytest.mark.parametrize( "seconds, expected", [ @@ -2377,8 +2545,8 @@ class TestCodeHighlight: result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {kw}def{hl-reset} {function}test_foo{hl-reset}():", - "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}", + " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", + "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}", "{bold}{red}E assert 1 == 10{reset}", ] ) @@ -2399,9 +2567,9 @@ class TestCodeHighlight: result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {kw}def{hl-reset} {function}test_foo{hl-reset}():", + " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", " {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}", - "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}", + "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}", "{bold}{red}E assert 0{reset}", ] ) @@ -2422,8 +2590,8 @@ class TestCodeHighlight: result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {kw}def{hl-reset} {function}test_foo{hl-reset}():", - "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}", + " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", + "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}", "{bold}{red}E assert 1 == 10{reset}", ] ) @@ -2484,3 +2652,355 @@ def test_format_trimmed() -> None: assert _format_trimmed(" ({}) ", msg, len(msg) + 4) == " (unconditional skip) " assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) " + + +class TestFineGrainedTestCase: + DEFAULT_FILE_CONTENTS = """ + import pytest + + @pytest.mark.parametrize("i", range(4)) + def test_ok(i): + ''' + some docstring + ''' + pass + + def test_fail(): + assert False + """ + LONG_SKIP_FILE_CONTENTS = """ + import pytest + + @pytest.mark.skip( + "some long skip reason that will not fit on a single line with other content that goes" + " on and on and on and on and on" + ) + def test_skip(): + pass + """ + + @pytest.mark.parametrize("verbosity", [1, 2]) + def test_execute_positive(self, verbosity, pytester: Pytester) -> None: + # expected: one test case per line (with file name), word describing result + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=verbosity) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + "collected 5 items", + "", + f"{p.name}::test_ok[0] PASSED [ 20%]", + f"{p.name}::test_ok[1] PASSED [ 40%]", + f"{p.name}::test_ok[2] PASSED [ 60%]", + f"{p.name}::test_ok[3] PASSED [ 80%]", + f"{p.name}::test_fail FAILED [100%]", + ], + consecutive=True, + ) + + def test_execute_0_global_1(self, pytester: Pytester) -> None: + # expected: one file name per line, single character describing result + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=0) + result = pytester.runpytest("-v", p) + + result.stdout.fnmatch_lines( + [ + "collecting ... collected 5 items", + "", + f"{p.name} ....F [100%]", + ], + consecutive=True, + ) + + @pytest.mark.parametrize("verbosity", [-1, -2]) + def test_execute_negative(self, verbosity, pytester: Pytester) -> None: + # expected: single character describing result + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=verbosity) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + "collected 5 items", + "....F [100%]", + ], + consecutive=True, + ) + + def test_execute_skipped_positive_2(self, pytester: Pytester) -> None: + # expected: one test case per line (with file name), word describing result, full reason + p = TestFineGrainedTestCase._initialize_files( + pytester, + verbosity=2, + file_contents=TestFineGrainedTestCase.LONG_SKIP_FILE_CONTENTS, + ) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "", + f"{p.name}::test_skip SKIPPED (some long skip", + "reason that will not fit on a single line with other content that goes", + "on and on and on and on and on) [100%]", + ], + consecutive=True, + ) + + def test_execute_skipped_positive_1(self, pytester: Pytester) -> None: + # expected: one test case per line (with file name), word describing result, reason truncated + p = TestFineGrainedTestCase._initialize_files( + pytester, + verbosity=1, + file_contents=TestFineGrainedTestCase.LONG_SKIP_FILE_CONTENTS, + ) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "", + f"{p.name}::test_skip SKIPPED (some long ski...) [100%]", + ], + consecutive=True, + ) + + def test_execute_skipped__0_global_1(self, pytester: Pytester) -> None: + # expected: one file name per line, single character describing result (no reason) + p = TestFineGrainedTestCase._initialize_files( + pytester, + verbosity=0, + file_contents=TestFineGrainedTestCase.LONG_SKIP_FILE_CONTENTS, + ) + result = pytester.runpytest("-v", p) + + result.stdout.fnmatch_lines( + [ + "collecting ... collected 1 item", + "", + f"{p.name} s [100%]", + ], + consecutive=True, + ) + + @pytest.mark.parametrize("verbosity", [-1, -2]) + def test_execute_skipped_negative(self, verbosity, pytester: Pytester) -> None: + # expected: single character describing result (no reason) + p = TestFineGrainedTestCase._initialize_files( + pytester, + verbosity=verbosity, + file_contents=TestFineGrainedTestCase.LONG_SKIP_FILE_CONTENTS, + ) + result = pytester.runpytest(p) + + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "s [100%]", + ], + consecutive=True, + ) + + @pytest.mark.parametrize("verbosity", [1, 2]) + def test__collect_only_positive(self, verbosity, pytester: Pytester) -> None: + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=verbosity) + result = pytester.runpytest("--collect-only", p) + + result.stdout.fnmatch_lines( + [ + "collected 5 items", + "", + f"<Dir {p.parent.name}>", + f" <Module {p.name}>", + " <Function test_ok[0]>", + " some docstring", + " <Function test_ok[1]>", + " some docstring", + " <Function test_ok[2]>", + " some docstring", + " <Function test_ok[3]>", + " some docstring", + " <Function test_fail>", + ], + consecutive=True, + ) + + def test_collect_only_0_global_1(self, pytester: Pytester) -> None: + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=0) + result = pytester.runpytest("-v", "--collect-only", p) + + result.stdout.fnmatch_lines( + [ + "collecting ... collected 5 items", + "", + f"<Dir {p.parent.name}>", + f" <Module {p.name}>", + " <Function test_ok[0]>", + " <Function test_ok[1]>", + " <Function test_ok[2]>", + " <Function test_ok[3]>", + " <Function test_fail>", + ], + consecutive=True, + ) + + def test_collect_only_negative_1(self, pytester: Pytester) -> None: + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=-1) + result = pytester.runpytest("--collect-only", p) + + result.stdout.fnmatch_lines( + [ + "collected 5 items", + "", + f"{p.name}::test_ok[0]", + f"{p.name}::test_ok[1]", + f"{p.name}::test_ok[2]", + f"{p.name}::test_ok[3]", + f"{p.name}::test_fail", + ], + consecutive=True, + ) + + def test_collect_only_negative_2(self, pytester: Pytester) -> None: + p = TestFineGrainedTestCase._initialize_files(pytester, verbosity=-2) + result = pytester.runpytest("--collect-only", p) + + result.stdout.fnmatch_lines( + [ + "collected 5 items", + "", + f"{p.name}: 5", + ], + consecutive=True, + ) + + @staticmethod + def _initialize_files( + pytester: Pytester, verbosity: int, file_contents: str = DEFAULT_FILE_CONTENTS + ) -> Path: + p = pytester.makepyfile(file_contents) + pytester.makeini( + f""" + [pytest] + verbosity_test_cases = {verbosity} + """ + ) + return p + + +def test_summary_xfail_reason(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail + def test_xfail(): + assert False + + @pytest.mark.xfail(reason="foo") + def test_xfail_reason(): + assert False + """ + ) + result = pytester.runpytest("-rx") + expect1 = "XFAIL test_summary_xfail_reason.py::test_xfail" + expect2 = "XFAIL test_summary_xfail_reason.py::test_xfail_reason - foo" + result.stdout.fnmatch_lines([expect1, expect2]) + assert result.stdout.lines.count(expect1) == 1 + assert result.stdout.lines.count(expect2) == 1 + + +def test_summary_xfail_tb(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail + def test_xfail(): + a, b = 1, 2 + assert a == b + """ + ) + result = pytester.runpytest("-rx") + result.stdout.fnmatch_lines( + [ + "*= XFAILURES =*", + "*_ test_xfail _*", + "* @pytest.mark.xfail*", + "* def test_xfail():*", + "* a, b = 1, 2*", + "> *assert a == b*", + "E *assert 1 == 2*", + "test_summary_xfail_tb.py:6: AssertionError*", + "*= short test summary info =*", + "XFAIL test_summary_xfail_tb.py::test_xfail", + "*= 1 xfailed in * =*", + ] + ) + + +def test_xfail_tb_line(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail + def test_xfail(): + a, b = 1, 2 + assert a == b + """ + ) + result = pytester.runpytest("-rx", "--tb=line") + result.stdout.fnmatch_lines( + [ + "*= XFAILURES =*", + "*test_xfail_tb_line.py:6: assert 1 == 2", + "*= short test summary info =*", + "XFAIL test_xfail_tb_line.py::test_xfail", + "*= 1 xfailed in * =*", + ] + ) + + +def test_summary_xpass_reason(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail + def test_pass(): + ... + + @pytest.mark.xfail(reason="foo") + def test_reason(): + ... + """ + ) + result = pytester.runpytest("-rX") + expect1 = "XPASS test_summary_xpass_reason.py::test_pass" + expect2 = "XPASS test_summary_xpass_reason.py::test_reason - foo" + result.stdout.fnmatch_lines([expect1, expect2]) + assert result.stdout.lines.count(expect1) == 1 + assert result.stdout.lines.count(expect2) == 1 + + +def test_xpass_output(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.mark.xfail + def test_pass(): + print('hi there') + """ + ) + result = pytester.runpytest("-rX") + result.stdout.fnmatch_lines( + [ + "*= XPASSES =*", + "*_ test_pass _*", + "*- Captured stdout call -*", + "*= short test summary info =*", + "XPASS test_xpass_output.py::test_pass*", + "*= 1 xpassed in * =*", + ] + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_threadexception.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_threadexception.py index 5b7519f27d8..99837b94e8a 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_threadexception.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_threadexception.py @@ -1,11 +1,5 @@ -import sys - -import pytest from _pytest.pytester import Pytester - - -if sys.version_info < (3, 8): - pytest.skip("threadexception plugin needs Python>=3.8", allow_module_level=True) +import pytest @pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning") diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_tmpdir.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_tmpdir.py index 4f7c5384700..331ee7da6c7 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_tmpdir.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_tmpdir.py @@ -1,15 +1,15 @@ +# mypy: allow-untyped-defs +import dataclasses import os +from pathlib import Path import stat import sys -import warnings -from pathlib import Path from typing import Callable from typing import cast from typing import List +from typing import Union +import warnings -import attr - -import pytest from _pytest import pathlib from _pytest.config import Config from _pytest.monkeypatch import MonkeyPatch @@ -23,6 +23,7 @@ from _pytest.pathlib import rm_rf from _pytest.pytester import Pytester from _pytest.tmpdir import get_user from _pytest.tmpdir import TempPathFactory +import pytest def test_tmp_path_fixture(pytester: Pytester) -> None: @@ -31,9 +32,9 @@ def test_tmp_path_fixture(pytester: Pytester) -> None: results.stdout.fnmatch_lines(["*1 passed*"]) -@attr.s +@dataclasses.dataclass class FakeConfig: - basetemp = attr.ib() + basetemp: Union[str, Path] @property def trace(self): @@ -42,13 +43,21 @@ class FakeConfig: def get(self, key): return lambda *k: None + def getini(self, name): + if name == "tmp_path_retention_count": + return 3 + elif name == "tmp_path_retention_policy": + return "all" + else: + assert False + @property def option(self): return self class TestTmpPathHandler: - def test_mktemp(self, tmp_path): + def test_mktemp(self, tmp_path: Path) -> None: config = cast(Config, FakeConfig(tmp_path)) t = TempPathFactory.from_config(config, _ispytest=True) tmp = t.mktemp("world") @@ -59,7 +68,9 @@ class TestTmpPathHandler: assert str(tmp2.relative_to(t.getbasetemp())).startswith("this") assert tmp2 != tmp - def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): + def test_tmppath_relative_basetemp_absolute( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: """#4425""" monkeypatch.chdir(tmp_path) config = cast(Config, FakeConfig("hello")) @@ -84,6 +95,136 @@ class TestConfigTmpPath: assert mytemp.exists() assert not mytemp.joinpath("hello").exists() + def test_policy_failed_removes_only_passed_dir(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """ + def test_1(tmp_path): + assert 0 == 0 + def test_2(tmp_path): + assert 0 == 1 + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + tmp_path_retention_policy = "failed" + """ + ) + + pytester.inline_run(p) + root = pytester._test_tmproot + + for child in root.iterdir(): + base_dir = list( + filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir()) + ) + assert len(base_dir) == 1 + test_dir = list( + filter( + lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir() + ) + ) + # Check only the failed one remains + assert len(test_dir) == 1 + assert test_dir[0].name == "test_20" + + def test_policy_failed_removes_basedir_when_all_passed( + self, pytester: Pytester + ) -> None: + p = pytester.makepyfile( + """ + def test_1(tmp_path): + assert 0 == 0 + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + tmp_path_retention_policy = "failed" + """ + ) + + pytester.inline_run(p) + root = pytester._test_tmproot + for child in root.iterdir(): + # This symlink will be deleted by cleanup_numbered_dir **after** + # the test finishes because it's triggered by atexit. + # So it has to be ignored here. + base_dir = filter(lambda x: not x.is_symlink(), child.iterdir()) + # Check the base dir itself is gone + assert len(list(base_dir)) == 0 + + # issue #10502 + def test_policy_failed_removes_dir_when_skipped_from_fixture( + self, pytester: Pytester + ) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def fixt(tmp_path): + pytest.skip() + + def test_fixt(fixt): + pass + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + tmp_path_retention_policy = "failed" + """ + ) + + pytester.inline_run(p) + + # Check if the whole directory is removed + root = pytester._test_tmproot + for child in root.iterdir(): + base_dir = list( + filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir()) + ) + assert len(base_dir) == 0 + + # issue #10502 + def test_policy_all_keeps_dir_when_skipped_from_fixture( + self, pytester: Pytester + ) -> None: + p = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def fixt(tmp_path): + pytest.skip() + + def test_fixt(fixt): + pass + """ + ) + pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + tmp_path_retention_policy = "all" + """ + ) + pytester.inline_run(p) + + # Check if the whole directory is kept + root = pytester._test_tmproot + for child in root.iterdir(): + base_dir = list( + filter(lambda x: x.is_dir() and not x.is_symlink(), child.iterdir()) + ) + assert len(base_dir) == 1 + test_dir = list( + filter( + lambda x: x.is_dir() and not x.is_symlink(), base_dir[0].iterdir() + ) + ) + assert len(test_dir) == 1 + testdata = [ ("mypath", True), @@ -101,12 +242,10 @@ testdata = [ def test_mktemp(pytester: Pytester, basename: str, is_ok: bool) -> None: mytemp = pytester.mkdir("mytemp") p = pytester.makepyfile( - """ + f""" def test_abs_path(tmp_path_factory): - tmp_path_factory.mktemp('{}', numbered=False) - """.format( - basename - ) + tmp_path_factory.mktemp('{basename}', numbered=False) + """ ) result = pytester.runpytest(p, "--basetemp=%s" % mytemp) @@ -197,7 +336,6 @@ def test_tmp_path_fallback_uid_not_found(pytester: Pytester) -> None: """Test that tmp_path works even if the current process's user id does not correspond to a valid user. """ - pytester.makepyfile( """ def test_some(tmp_path): @@ -275,12 +413,12 @@ class TestNumberedDir: assert not lock.exists() - def _do_cleanup(self, tmp_path: Path) -> None: + def _do_cleanup(self, tmp_path: Path, keep: int = 2) -> None: self.test_make(tmp_path) cleanup_numbered_dir( root=tmp_path, prefix=self.PREFIX, - keep=2, + keep=keep, consider_lock_dead_if_created_before=0, ) @@ -289,6 +427,11 @@ class TestNumberedDir: a, b = (x for x in tmp_path.iterdir() if not x.is_symlink()) print(a, b) + def test_cleanup_keep_0(self, tmp_path: Path): + self._do_cleanup(tmp_path, 0) + dir_num = len(list(tmp_path.iterdir())) + assert dir_num == 0 + def test_cleanup_locked(self, tmp_path): p = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) @@ -367,33 +510,31 @@ class TestRmRf: # unknown exception with pytest.warns(pytest.PytestWarning): - exc_info1 = (None, RuntimeError(), None) + exc_info1 = (RuntimeError, RuntimeError(), None) on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path) assert fn.is_file() # we ignore FileNotFoundError - exc_info2 = (None, FileNotFoundError(), None) + exc_info2 = (FileNotFoundError, FileNotFoundError(), None) assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path) # unknown function with pytest.warns( pytest.PytestWarning, - match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ", + match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ", ): - exc_info3 = (None, PermissionError(), None) + exc_info3 = (PermissionError, PermissionError(), None) on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path) assert fn.is_file() # ignored function - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - with pytest.warns(None) as warninfo: # type: ignore[call-overload] - exc_info4 = (None, PermissionError(), None) - on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) - assert fn.is_file() - assert not [x.message for x in warninfo] - - exc_info5 = (None, PermissionError(), None) + with warnings.catch_warnings(record=True) as w: + exc_info4 = PermissionError() + on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) + assert fn.is_file() + assert not [x.message for x in w] + + exc_info5 = PermissionError() on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path) assert not fn.is_file() @@ -416,7 +557,7 @@ def test_basetemp_with_read_only_files(pytester: Pytester) -> None: def test(tmp_path): fn = tmp_path / 'foo.txt' - fn.write_text('hello') + fn.write_text('hello', encoding='utf-8') mode = os.stat(str(fn)).st_mode os.chmod(str(fn), mode & ~stat.S_IREAD) """ @@ -446,7 +587,7 @@ def test_tmp_path_factory_create_directory_with_safe_permissions( """Verify that pytest creates directories under /tmp with private permissions.""" # Use the test's tmp_path as the system temproot (/tmp). monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path)) - tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True) + tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) basetemp = tmp_factory.getbasetemp() # No world-readable permissions. @@ -466,14 +607,14 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions( """ # Use the test's tmp_path as the system temproot (/tmp). monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path)) - tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True) + tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) basetemp = tmp_factory.getbasetemp() # Before - simulate bad perms. os.chmod(basetemp.parent, 0o777) assert (basetemp.parent.stat().st_mode & 0o077) != 0 - tmp_factory = TempPathFactory(None, lambda *args: None, _ispytest=True) + tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True) basetemp = tmp_factory.getbasetemp() # After - fixed. diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_unittest.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_unittest.py index 1601086d5b2..96223b22a2e 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_unittest.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_unittest.py @@ -1,11 +1,12 @@ +# mypy: allow-untyped-defs import gc import sys from typing import List -import pytest from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest def test_simple_unittest(pytester: Pytester) -> None: @@ -207,10 +208,14 @@ def test_teardown_issue1649(pytester: Pytester) -> None: """ ) + pytester.inline_run("-s", testpath) gc.collect() + + # Either already destroyed, or didn't run setUp. for obj in gc.get_objects(): - assert type(obj).__name__ != "TestCaseObjectsShouldBeCleanedUp" + if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp": + assert not hasattr(obj, "an_expensive_obj") def test_unittest_skip_issue148(pytester: Pytester) -> None: @@ -294,7 +299,7 @@ def test_setup_setUpClass(pytester: Pytester) -> None: @classmethod def tearDownClass(cls): cls.x -= 1 - def test_teareddown(): + def test_torn_down(): assert MyTestCase.x == 0 """ ) @@ -341,7 +346,7 @@ def test_setup_class(pytester: Pytester) -> None: assert self.x == 1 def teardown_class(cls): cls.x -= 1 - def test_teareddown(): + def test_torn_down(): assert MyTestCase.x == 0 """ ) @@ -352,22 +357,21 @@ def test_setup_class(pytester: Pytester) -> None: @pytest.mark.parametrize("type", ["Error", "Failure"]) def test_testcase_adderrorandfailure_defers(pytester: Pytester, type: str) -> None: pytester.makepyfile( - """ + f""" from unittest import TestCase import pytest class MyTestCase(TestCase): def run(self, result): excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) try: - result.add%s(self, excinfo._excinfo) + result.add{type}(self, excinfo._excinfo) except KeyboardInterrupt: raise except: - pytest.fail("add%s should not raise") + pytest.fail("add{type} should not raise") def test_hello(self): pass """ - % (type, type) ) result = pytester.runpytest() result.stdout.no_fnmatch_line("*should not raise*") @@ -399,14 +403,13 @@ def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None: mp.setattr(_pytest._code, 'ExceptionInfo', FakeExceptionInfo) try: excinfo = excinfo._excinfo - result.add%(type)s(self, excinfo) + result.add{type}(self, excinfo) finally: mp.undo() def test_hello(self): pass - """ - % locals() + """.format(**locals()) ) result = pytester.runpytest() result.stdout.fnmatch_lines( @@ -833,7 +836,7 @@ def test_unittest_expected_failure_for_passing_test_is_fail( @pytest.mark.parametrize("stmt", ["return", "yield"]) def test_unittest_setup_interaction(pytester: Pytester, stmt: str) -> None: pytester.makepyfile( - """ + f""" import unittest import pytest class MyTestCase(unittest.TestCase): @@ -855,9 +858,7 @@ def test_unittest_setup_interaction(pytester: Pytester, stmt: str) -> None: def test_classattr(self): assert self.__class__.hello == "world" - """.format( - stmt=stmt - ) + """ ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) @@ -880,7 +881,7 @@ def test_non_unittest_no_setupclass_support(pytester: Pytester) -> None: def tearDownClass(cls): cls.x = 1 - def test_not_teareddown(): + def test_not_torn_down(): assert TestFoo.x == 0 """ @@ -952,7 +953,7 @@ def test_issue333_result_clearing(pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(item): yield assert 0 @@ -1062,7 +1063,7 @@ def test_usefixtures_marker_on_unittest(base, pytester: Pytester) -> None: ) pytester.makepyfile( - """ + f""" import pytest import {module} @@ -1081,9 +1082,7 @@ def test_usefixtures_marker_on_unittest(base, pytester: Pytester) -> None: assert self.fixture2 - """.format( - module=module, base=base - ) + """ ) result = pytester.runpytest("-s") @@ -1241,33 +1240,69 @@ def test_pdb_teardown_called(pytester: Pytester, monkeypatch: MonkeyPatch) -> No @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) -def test_pdb_teardown_skipped( +def test_pdb_teardown_skipped_for_functions( pytester: Pytester, monkeypatch: MonkeyPatch, mark: str ) -> None: - """With --pdb, setUp and tearDown should not be called for skipped tests.""" + """ + With --pdb, setUp and tearDown should not be called for tests skipped + via a decorator (#7215). + """ tracked: List[str] = [] - monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) + monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) pytester.makepyfile( - """ + f""" import unittest import pytest class MyTestCase(unittest.TestCase): def setUp(self): - pytest.test_pdb_teardown_skipped.append("setUp:" + self.id()) + pytest.track_pdb_teardown_skipped.append("setUp:" + self.id()) def tearDown(self): - pytest.test_pdb_teardown_skipped.append("tearDown:" + self.id()) + pytest.track_pdb_teardown_skipped.append("tearDown:" + self.id()) {mark}("skipped for reasons") def test_1(self): pass - """.format( - mark=mark - ) + """ + ) + result = pytester.runpytest_inprocess("--pdb") + result.stdout.fnmatch_lines("* 1 skipped in *") + assert tracked == [] + + +@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) +def test_pdb_teardown_skipped_for_classes( + pytester: Pytester, monkeypatch: MonkeyPatch, mark: str +) -> None: + """ + With --pdb, setUp and tearDown should not be called for tests skipped + via a decorator on the class (#10060). + """ + tracked: List[str] = [] + monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) + + pytester.makepyfile( + f""" + import unittest + import pytest + + {mark}("skipped for reasons") + class MyTestCase(unittest.TestCase): + + def setUp(self): + pytest.track_pdb_teardown_skipped.append("setUp:" + self.id()) + + def tearDown(self): + pytest.track_pdb_teardown_skipped.append("tearDown:" + self.id()) + + def test_1(self): + pass + + """ ) result = pytester.runpytest_inprocess("--pdb") result.stdout.fnmatch_lines("* 1 skipped in *") @@ -1314,9 +1349,6 @@ def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None: result.stdout.fnmatch_lines(expected_lines) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_success(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1342,9 +1374,6 @@ def test_do_class_cleanups_on_success(pytester: Pytester) -> None: assert passed == 3 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1369,9 +1398,6 @@ def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: assert passed == 1 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_teardownclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1474,6 +1500,95 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None: assert passed == 1 +class TestClassCleanupErrors: + """ + Make sure to show exceptions raised during class cleanup function (those registered + via addClassCleanup()). + + See #11728. + """ + + def test_class_cleanups_failure_in_setup(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + def cleanup(n): + raise Exception(f"fail {n}") + cls.addClassCleanup(cleanup, 2) + cls.addClassCleanup(cleanup, 1) + raise Exception("fail 0") + def test(self): + pass + """ + ) + result = pytester.runpytest("-s", testpath) + result.assert_outcomes(passed=0, errors=1) + result.stdout.fnmatch_lines( + [ + "*Unittest class cleanup errors *2 sub-exceptions*", + "*Exception: fail 1", + "*Exception: fail 2", + ] + ) + result.stdout.fnmatch_lines( + [ + "* ERROR at setup of MyTestCase.test *", + "E * Exception: fail 0", + ] + ) + + def test_class_cleanups_failure_in_teardown(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + def cleanup(n): + raise Exception(f"fail {n}") + cls.addClassCleanup(cleanup, 2) + cls.addClassCleanup(cleanup, 1) + def test(self): + pass + """ + ) + result = pytester.runpytest("-s", testpath) + result.assert_outcomes(passed=1, errors=1) + result.stdout.fnmatch_lines( + [ + "*Unittest class cleanup errors *2 sub-exceptions*", + "*Exception: fail 1", + "*Exception: fail 2", + ] + ) + + def test_class_cleanup_1_failure_in_teardown(self, pytester: Pytester) -> None: + testpath = pytester.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + def cleanup(n): + raise Exception(f"fail {n}") + cls.addClassCleanup(cleanup, 1) + def test(self): + pass + """ + ) + result = pytester.runpytest("-s", testpath) + result.assert_outcomes(passed=1, errors=1) + result.stdout.fnmatch_lines( + [ + "*ERROR at teardown of MyTestCase.test*", + "*Exception: fail 1", + ] + ) + + def test_traceback_pruning(pytester: Pytester) -> None: """Regression test for #9610 - doesn't crash during traceback pruning.""" pytester.makepyfile( @@ -1498,3 +1613,30 @@ def test_traceback_pruning(pytester: Pytester) -> None: assert passed == 1 assert failed == 1 assert reprec.ret == 1 + + +def test_raising_unittest_skiptest_during_collection( + pytester: Pytester, +) -> None: + pytester.makepyfile( + """ + import unittest + + class TestIt(unittest.TestCase): + def test_it(self): pass + def test_it2(self): pass + + raise unittest.SkipTest() + + class TestIt2(unittest.TestCase): + def test_it(self): pass + def test_it2(self): pass + """ + ) + reprec = pytester.inline_run() + passed, skipped, failed = reprec.countoutcomes() + assert passed == 0 + # Unittest reports one fake test for a skipped module. + assert skipped == 1 + assert failed == 0 + assert reprec.ret == ExitCode.NO_TESTS_COLLECTED diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_unraisableexception.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_unraisableexception.py index f625833dcea..1657cfe4a84 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_unraisableexception.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_unraisableexception.py @@ -1,13 +1,13 @@ import sys -import pytest from _pytest.pytester import Pytester +import pytest -if sys.version_info < (3, 8): - pytest.skip("unraisableexception plugin needs Python>=3.8", allow_module_level=True) +PYPY = hasattr(sys, "pypy_version_info") +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile( @@ -40,6 +40,7 @@ def test_unraisable(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_setup(pytester: Pytester) -> None: pytester.makepyfile( @@ -76,6 +77,7 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_teardown(pytester: Pytester) -> None: pytester.makepyfile( @@ -116,7 +118,7 @@ def test_unraisable_in_teardown(pytester: Pytester) -> None: @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_unraisable_warning_error(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" class BrokenDel: def __del__(self) -> None: raise ValueError("del is broken") @@ -124,6 +126,7 @@ def test_unraisable_warning_error(pytester: Pytester) -> None: def test_it() -> None: obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_2(): pass """ diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_warning_types.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_warning_types.py index b49cc68f9c6..a50d278bde2 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_warning_types.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_warning_types.py @@ -1,8 +1,9 @@ +# mypy: allow-untyped-defs import inspect -import pytest from _pytest import warning_types from _pytest.pytester import Pytester +import pytest @pytest.mark.parametrize( @@ -36,3 +37,12 @@ def test_pytest_warnings_repr_integration_test(pytester: Pytester) -> None: ) result = pytester.runpytest() result.stdout.fnmatch_lines(["E pytest.PytestWarning: some warning"]) + + +@pytest.mark.filterwarnings("error") +def test_warn_explicit_for_annotates_errors_with_location(): + with pytest.raises(Warning, match="(?m)test\n at .*python_api.py:\\d+"): + warning_types.warn_explicit_for( + pytest.raises, # type: ignore[arg-type] + warning_types.PytestWarning("test"), + ) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/test_warnings.py b/tests/wpt/tests/tools/third_party/pytest/testing/test_warnings.py index 5663c46cead..3ef0cd3b546 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/test_warnings.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/test_warnings.py @@ -1,12 +1,15 @@ +# mypy: allow-untyped-defs import os -import warnings +import sys from typing import List from typing import Optional from typing import Tuple +import warnings -import pytest from _pytest.fixtures import FixtureRequest from _pytest.pytester import Pytester +import pytest + WARNINGS_SUMMARY_HEADER = "warnings summary" @@ -15,16 +18,13 @@ WARNINGS_SUMMARY_HEADER = "warnings summary" def pyfile_with_warnings(pytester: Pytester, request: FixtureRequest) -> str: """Create a test file which calls a function in a module which generates warnings.""" pytester.syspathinsert() - test_name = request.function.__name__ - module_name = test_name.lstrip("test_") + "_module" + module_name = request.function.__name__[len("test_") :] + "_module" test_file = pytester.makepyfile( - """ + f""" import {module_name} def test_func(): assert {module_name}.foo() == 1 - """.format( - module_name=module_name - ), + """, **{ module_name: """ import warnings @@ -239,7 +239,7 @@ def test_filterwarnings_mark_registration(pytester: Pytester) -> None: @pytest.mark.filterwarnings("always::UserWarning") -def test_warning_captured_hook(pytester: Pytester) -> None: +def test_warning_recorded_hook(pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_configure(config): @@ -276,9 +276,9 @@ def test_warning_captured_hook(pytester: Pytester) -> None: expected = [ ("config warning", "config", ""), ("collect warning", "collect", ""), - ("setup warning", "runtest", "test_warning_captured_hook.py::test_func"), - ("call warning", "runtest", "test_warning_captured_hook.py::test_func"), - ("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"), + ("setup warning", "runtest", "test_warning_recorded_hook.py::test_func"), + ("call warning", "runtest", "test_warning_recorded_hook.py::test_func"), + ("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"), ] for index in range(len(expected)): collected_result = collected[index] @@ -435,7 +435,7 @@ class TestDeprecationWarningsByDefault: def create_file(self, pytester: Pytester, mark="") -> None: pytester.makepyfile( - """ + f""" import pytest, warnings warnings.warn(DeprecationWarning("collection")) @@ -443,9 +443,7 @@ class TestDeprecationWarningsByDefault: {mark} def test_foo(): warnings.warn(PendingDeprecationWarning("test run")) - """.format( - mark=mark - ) + """ ) @pytest.mark.parametrize("customize_filters", [True, False]) @@ -517,6 +515,7 @@ class TestDeprecationWarningsByDefault: assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() +@pytest.mark.skip("not relevant until pytest 9.0") @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> None: """This ensures that PytestRemovedInXWarnings raised by pytest are turned into errors. @@ -528,7 +527,7 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No """ import warnings, pytest def test(): - warnings.warn(pytest.PytestRemovedIn7Warning("some warning")) + warnings.warn(pytest.PytestRemovedIn9Warning("some warning")) """ ) if change_default == "ini": @@ -536,12 +535,12 @@ def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> No """ [pytest] filterwarnings = - ignore::pytest.PytestRemovedIn7Warning + ignore::pytest.PytestRemovedIn9Warning """ ) args = ( - ("-Wignore::pytest.PytestRemovedIn7Warning",) + ("-Wignore::pytest.PytestRemovedIn9Warning",) if change_default == "cmdline" else () ) @@ -621,11 +620,11 @@ def test_group_warnings_by_message_summary(pytester: Pytester) -> None: "*== %s ==*" % WARNINGS_SUMMARY_HEADER, "test_1.py: 21 warnings", "test_2.py: 1 warning", - " */test_1.py:7: UserWarning: foo", + " */test_1.py:8: UserWarning: foo", " warnings.warn(UserWarning(msg))", "", "test_1.py: 20 warnings", - " */test_1.py:7: UserWarning: bar", + " */test_1.py:8: UserWarning: bar", " warnings.warn(UserWarning(msg))", "", "-- Docs: *", @@ -773,3 +772,71 @@ class TestStackLevel: "*Unknown pytest.mark.unknown*", ] ) + + +def test_warning_on_testpaths_not_found(pytester: Pytester) -> None: + # Check for warning when testpaths set, but not found by glob + pytester.makeini( + """ + [pytest] + testpaths = absent + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + ["*ConfigWarning: No files were found in testpaths*", "*1 warning*"] + ) + + +def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None: + # Some platforms (notably PyPy) don't have tracemalloc. + # We choose to explicitly not skip this in case tracemalloc is not + # available, using `importorskip("tracemalloc")` for example, + # because we want to ensure the same code path does not break in those platforms. + try: + import tracemalloc # noqa: F401 + + has_tracemalloc = True + except ImportError: + has_tracemalloc = False + + # Explicitly disable PYTHONTRACEMALLOC in case pytest's test suite is running + # with it enabled. + monkeypatch.delenv("PYTHONTRACEMALLOC", raising=False) + + pytester.makepyfile( + """ + def open_file(p): + f = p.open("r", encoding="utf-8") + assert p.read_text() == "hello" + + def test_resource_warning(tmp_path): + p = tmp_path.joinpath("foo.txt") + p.write_text("hello", encoding="utf-8") + open_file(p) + """ + ) + result = pytester.run(sys.executable, "-Xdev", "-m", "pytest") + expected_extra = ( + [ + "*ResourceWarning* unclosed file*", + "*Enable tracemalloc to get traceback where the object was allocated*", + "*See https* for more info.", + ] + if has_tracemalloc + else [] + ) + result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"]) + + monkeypatch.setenv("PYTHONTRACEMALLOC", "20") + + result = pytester.run(sys.executable, "-Xdev", "-m", "pytest") + expected_extra = ( + [ + "*ResourceWarning* unclosed file*", + "*Object allocated at*", + ] + if has_tracemalloc + else [] + ) + result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"]) diff --git a/tests/wpt/tests/tools/third_party/pytest/testing/typing_checks.py b/tests/wpt/tests/tools/third_party/pytest/testing/typing_checks.py index 0a6b5ad2841..4b146a25110 100644 --- a/tests/wpt/tests/tools/third_party/pytest/testing/typing_checks.py +++ b/tests/wpt/tests/tools/third_party/pytest/testing/typing_checks.py @@ -1,9 +1,17 @@ +# mypy: allow-untyped-defs """File for checking typing issues. This file is not executed, it is only checked by mypy to ensure that none of the code triggers any mypy errors. """ + +import contextlib +from typing import Optional + +from typing_extensions import assert_type + import pytest +from pytest import MonkeyPatch # Issue #7488. @@ -22,3 +30,22 @@ def check_fixture_ids_callable() -> None: @pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__)) def check_parametrize_ids_callable(func) -> None: pass + + +# Issue #10999. +def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: + from typing import TypedDict + + class Foo(TypedDict): + x: int + y: float + + a: Foo = {"x": 1, "y": 3.14} + monkeypatch.setitem(a, "x", 2) + monkeypatch.delitem(a, "y") + + +def check_raises_is_a_context_manager(val: bool) -> None: + with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: + pass + assert_type(excinfo, Optional[pytest.ExceptionInfo[RuntimeError]]) diff --git a/tests/wpt/tests/tools/third_party/pytest/tox.ini b/tests/wpt/tests/tools/third_party/pytest/tox.ini index b2f90008ce1..30d3e68defc 100644 --- a/tests/wpt/tests/tools/third_party/pytest/tox.ini +++ b/tests/wpt/tests/tools/third_party/pytest/tox.ini @@ -4,20 +4,25 @@ minversion = 3.20.0 distshare = {homedir}/.tox/distshare envlist = linting - py36 - py37 py38 py39 py310 py311 + py312 + py313 pypy3 - py37-{pexpect,xdist,unittestextras,numpy,pluggymain} + py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} doctesting + doctesting-coverage plugins - py37-freeze + py38-freeze docs docs-checklinks + # checks that 3.11 native ExceptionGroup works with exceptiongroup + # not included in CI. + py311-exceptiongroup + [testenv] @@ -26,12 +31,20 @@ commands = doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest coverage: coverage combine coverage: coverage report -m -passenv = USER USERNAME COVERAGE_* PYTEST_ADDOPTS TERM SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST +passenv = + COVERAGE_* + PYTEST_ADDOPTS + TERM + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST setenv = _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_DOCTESTING:} {env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:} + # See https://docs.python.org/3/library/io.html#io-encoding-warning + # If we don't enable this, neither can any of our downstream users! + PYTHONWARNDEFAULTENCODING=1 + # Configuration to run with coverage similar to CI, e.g. - # "tox -e py37-coverage". + # "tox -e py38-coverage". coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess coverage: COVERAGE_FILE={toxinidir}/.coverage @@ -44,12 +57,14 @@ setenv = lsof: _PYTEST_TOX_POSARGS_LSOF=--lsof xdist: _PYTEST_TOX_POSARGS_XDIST=-n auto -extras = testing +extras = dev deps = doctesting: PyYAML + exceptiongroup: exceptiongroup>=1.0.0rc8 numpy: numpy>=1.19.4 pexpect: pexpect>=4.8.0 pluggymain: pluggy @ git+https://github.com/pytest-dev/pluggy.git + pylib: py>=1.8.2 unittestextras: twisted unittestextras: asynctest xdist: pytest-xdist>=2.1.0 @@ -61,6 +76,9 @@ skip_install = True basepython = python3 deps = pre-commit>=2.9.3 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} +setenv = + # pre-commit and tools it launches are not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs] basepython = python3 @@ -75,6 +93,9 @@ commands = # changelog in the docs; this does not happen on ReadTheDocs because it uses # the standard sphinx command so the 'changelog_towncrier_draft' is never set there sphinx-build -W --keep-going -b html doc/en doc/en/_build/html -t changelog_towncrier_draft {posargs:} +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs-checklinks] basepython = python3 @@ -83,34 +104,42 @@ changedir = doc/en deps = -r{toxinidir}/doc/en/requirements.txt commands = sphinx-build -W -q --keep-going -b linkcheck . _build +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:regen] changedir = doc/en basepython = python3 -passenv = SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST +passenv = + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST deps = - dataclasses PyYAML regendoc>=0.8.1 sphinx -whitelist_externals = +allowlist_externals = make commands = make regen +setenv = + # We don't want this warning to reach regen output. + PYTHONWARNDEFAULTENCODING= [testenv:plugins] # use latest versions of all plugins, including pre-releases pip_pre=true -# use latest pip and new dependency resolver (#7783) +# use latest pip to get new dependency resolver (#7783) download=true -install_command=python -m pip --use-feature=2020-resolver install {opts} {packages} +install_command=python -m pip install {opts} {packages} changedir = testing/plugins_integration deps = -rtesting/plugins_integration/requirements.txt setenv = PYTHONPATH=. +# Command temporarily removed until pytest-bdd is fixed: +# https://github.com/pytest-dev/pytest/pull/11785 +# pytest bdd_wallet.py commands = pip check - pytest bdd_wallet.py pytest --cov=. simple_integration.py pytest --ds=django_settings simple_integration.py pytest --html=simple.html simple_integration.py @@ -122,7 +151,7 @@ commands = pytest pytest_twisted_integration.py pytest simple_integration.py --force-sugar --flakes -[testenv:py37-freeze] +[testenv:py38-freeze] changedir = testing/freeze deps = pyinstaller @@ -151,34 +180,10 @@ passenv = {[testenv:release]passenv} deps = {[testenv:release]deps} commands = python scripts/prepare-release-pr.py {posargs} -[testenv:publish-gh-release-notes] -description = create GitHub release after deployment +[testenv:generate-gh-release-notes] +description = generate release notes that can be published as GitHub Release basepython = python3 usedevelop = True -passenv = GH_RELEASE_NOTES_TOKEN GITHUB_REF GITHUB_REPOSITORY deps = - github3.py pypandoc -commands = python scripts/publish-gh-release-notes.py {posargs} - -[flake8] -max-line-length = 120 -extend-ignore = - ; whitespace before ':' - E203 - ; Missing Docstrings - D100,D101,D102,D103,D104,D105,D106,D107 - ; Whitespace Issues - D202,D203,D204,D205,D209,D213 - ; Quotes Issues - D302 - ; Docstring Content Issues - D400,D401,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D415,D416,D417 - - -[isort] -; This config mimics what reorder-python-imports does. -force_single_line = 1 -known_localfolder = pytest,_pytest -known_third_party = test_source,test_excinfo -force_alphabetical_sort_within_sections = 1 +commands = python scripts/generate-gh-release-notes.py {posargs} diff --git a/tests/wpt/tests/tools/third_party_modified/mozlog/mozlog/capture.py b/tests/wpt/tests/tools/third_party_modified/mozlog/mozlog/capture.py index 75717d62c8a..78da63be417 100644 --- a/tests/wpt/tests/tools/third_party_modified/mozlog/mozlog/capture.py +++ b/tests/wpt/tests/tools/third_party_modified/mozlog/mozlog/capture.py @@ -88,7 +88,7 @@ class CaptureIO(object): while not self.logging_queue.empty(): try: self.logger.warning( - "Dropping log message: %r", self.logging_queue.get() + "Dropping log message: %r" % self.logging_queue.get() ) except Exception: pass diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome_ios.py b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome_ios.py index 20fcb908898..db81b91957b 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome_ios.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome_ios.py @@ -6,7 +6,6 @@ from .base import get_timeout_multiplier # noqa: F401 from ..environment import wait_for_service from ..executors import executor_kwargs as base_executor_kwargs from ..executors.base import WdspecExecutor # noqa: F401 -from ..executors.executorchrome import ChromeDriverPrintRefTestExecutor # noqa: F401 from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401 WebDriverTestharnessExecutor, # noqa: F401 WebDriverRefTestExecutor) # noqa: F401 @@ -17,7 +16,6 @@ __wptrunner__ = {"product": "chrome_ios", "browser": "ChromeiOSBrowser", "executor": {"testharness": "WebDriverTestharnessExecutor", "reftest": "WebDriverRefTestExecutor", - "print-reftest": "ChromeDriverPrintRefTestExecutor", "crashtest": "WebDriverCrashtestExecutor"}, "browser_kwargs": "browser_kwargs", "executor_kwargs": "executor_kwargs", diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 3e221b89798..f985d486759 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -696,6 +696,9 @@ class WebDriverRefTestExecutor(RefTestExecutor): """return [window.outerWidth - window.innerWidth, window.outerHeight - window.innerHeight];""" ) + # width_offset and height_offset should never be negative + width_offset = max(width_offset, 0) + height_offset = max(height_offset, 0) try: self.protocol.webdriver.window.position = (0, 0) except error.InvalidArgumentException: diff --git a/tests/wpt/tests/tools/wptserve/tests/test_stash.py b/tests/wpt/tests/tools/wptserve/tests/test_stash.py index 4157db5726d..7806dc1c9b2 100644 --- a/tests/wpt/tests/tools/wptserve/tests/test_stash.py +++ b/tests/wpt/tests/tools/wptserve/tests/test_stash.py @@ -77,6 +77,11 @@ def test_delayed_lock(add_cleanup): queue = multiprocessing.Queue() + # Reset internal state of the stash class + Stash._proxy = None + Stash.lock = None + Stash.manager = None + def mutex_lock_request(): """This request handler allows the caller to delay execution of a thread which has requested a proxied representation of the `lock` @@ -120,6 +125,11 @@ def test_delayed_dict(add_cleanup): queue = multiprocessing.Queue() + # Reset internal state of the stash class + Stash._proxy = None + Stash.lock = None + Stash.manager = None + # This request handler allows the caller to delay execution of a thread # which has requested a proxied representation of the "get_dict" property. def mutex_dict_request(): diff --git a/tests/wpt/tests/trusted-types/Element-toggleAttribute.html b/tests/wpt/tests/trusted-types/Element-toggleAttribute.html new file mode 100644 index 00000000000..c3b141f8cbd --- /dev/null +++ b/tests/wpt/tests/trusted-types/Element-toggleAttribute.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'"> +</head> +<body> +<div> + <p id="p" onclick="alert(1)"></p> + <iframe id="iframe" srcdoc="abc"></iframe> +</div> +<script> + // Regression test for crbug.com/341057803. + // This tests that TT doesn't interfere with regular DOM behaviour, and so + // these tests should pass on any browser, whether they support TT or not. + + test(t => { + const elem = document.getElementById("p"); + elem.toggleAttribute("onclick"); + assert_false(elem.hasAttribute("onclick")); + elem.toggleAttribute("onclick"); + assert_true(elem.hasAttribute("onclick")); + }, "TT should not interfere with toggleAttribute on an event handler"); + + test(t => { + const elem = document.getElementById("iframe"); + elem.toggleAttribute("srcdoc"); + assert_false(elem.hasAttribute("srcdoc")); + elem.toggleAttribute("srcdoc"); + assert_true(elem.hasAttribute("srcdoc")); + }, "TT should not interfere with toggleAttribute on an iframe srcdoc"); +</script> +</body> diff --git a/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-no-unsafe-eval.html b/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-no-unsafe-eval.html index 2b0922d2123..7a31866719f 100644 --- a/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-no-unsafe-eval.html +++ b/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-no-unsafe-eval.html @@ -91,20 +91,22 @@ expect_throws(_ => eval(scriptyPolicy.createScript('script_run_beacon="i ran"'))); flush(); assert_not_equals(script_run_beacon, 'i ran'); // Code did not run. + assert_equals(script_run_beacon, 'never_overwritten'); return p; }, "Trusted Type violation report: evaluating a Trusted Script violates script-src."); promise_test(t => { trustedTypes.createPolicy('default', { - createScript: s => s.replace('payload', 'default policy'), + createScript: s => s, }, true); let p = Promise.resolve() .then(promise_violation((e) => e.effectiveDirective.includes('script-src') && - e.sample.includes("default policy"))) + e.sample.includes("should not run"))) .then(promise_flush()); - expect_throws(_ => eval('script_run_beacon="payload"')); // script-src will block. - assert_not_equals(script_run_beacon, 'default policy'); // Code did not run. + expect_throws(_ => eval('script_run_beacon="should not run"')); // script-src will block. + assert_not_equals(script_run_beacon, 'should not run'); // Code did not run. + assert_equals(script_run_beacon, 'never_overwritten'); flush(); return p; }, "Trusted Type violation report: script-src restrictions apply after the default policy runs."); diff --git a/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-report-only.html b/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-report-only.html index 4e8ac5a2f43..0e7cf34bb14 100644 --- a/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-report-only.html +++ b/tests/wpt/tests/trusted-types/trusted-types-eval-reporting-report-only.html @@ -94,11 +94,11 @@ promise_test(t => { trustedTypes.createPolicy('default', { - createScript: s => s.replace('payload', 'default policy'), + createScript: s => s, }, true); let p = promise_flush()(); eval('script_run_beacon="payload"'); - assert_equals(script_run_beacon, 'default policy'); + assert_equals(script_run_beacon, 'payload'); flush(); return p; }, "Trusted Type violation report: default policy runs in report-only mode."); diff --git a/tests/wpt/tests/trusted-types/trusted-types-reporting.html b/tests/wpt/tests/trusted-types/trusted-types-reporting.html index 9db307db60d..42759093604 100644 --- a/tests/wpt/tests/trusted-types/trusted-types-reporting.html +++ b/tests/wpt/tests/trusted-types/trusted-types-reporting.html @@ -182,7 +182,7 @@ let p = Promise.resolve() .then(promise_violation("require-trusted-types-for 'script'")) .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLElement innerText|2+2;")); + .then(expect_sample("HTMLScriptElement innerText|2+2;")); expect_throws(_ => document.getElementById("script").innerText = "2+2;"); return p; }, "Trusted Type violation report: sample for script innerText assignment"); @@ -194,7 +194,7 @@ let p = Promise.resolve() .then(promise_violation("require-trusted-types-for 'script'")) .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("SVGAnimatedString baseVal")); + .then(expect_sample("SVGScriptElement href")); expect_throws(_ => { document.getElementById("svgscript").href.baseVal = "" }); return p; }, "Trusted Type violation report: sample for SVGScriptElement href assignment"); @@ -203,7 +203,7 @@ let p = Promise.resolve() .then(promise_violation("require-trusted-types-for 'script'")) .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("Element setAttribute")); + .then(expect_sample("SVGScriptElement href")); expect_throws(_ => { document.getElementById("svgscript").setAttribute('href', "test"); }); return p; }, "Trusted Type violation report: sample for SVGScriptElement href assignment by setAttribute"); @@ -234,7 +234,7 @@ let p = Promise.resolve() .then(promise_violation("require-trusted-types-for 'script'")) .then(expect_blocked_uri("trusted-types-sink")) - .then(expect_sample("HTMLElement innerText|abbb")) + .then(expect_sample("HTMLScriptElement innerText|abbb")) .then(e => assert_less_than(e.sample.length, 150)); const value = "a" + "b".repeat(50000); expect_throws(_ => document.getElementById("script").innerText = value); diff --git a/tests/wpt/tests/uievents/interface/keyboard-accesskey-click-event.html b/tests/wpt/tests/uievents/interface/keyboard-accesskey-click-event.html index f90101e31ef..93363c2e557 100644 --- a/tests/wpt/tests/uievents/interface/keyboard-accesskey-click-event.html +++ b/tests/wpt/tests/uievents/interface/keyboard-accesskey-click-event.html @@ -24,7 +24,7 @@ elementList.forEach((el)=>{eventList.forEach((ev)=>el.addEventListener(ev, (e)=> currentTest.step(()=>{ if(e instanceof PointerEvent){ // We want the test to run on all browsers even if click is not a PointerEvent. - assert_equals(e.pointerId, 0, "Click's pointerId has default value"); + assert_equals(e.pointerId, -1, "Click's pointerId has -1 because of non-pointing device input"); assert_equals(e.pointerType, "", "Click's pointerType has default value"); } assert_equals(e.screenX, 0, "Click's screenX has default value"); diff --git a/tests/wpt/tests/url/resources/urltestdata.json b/tests/wpt/tests/url/resources/urltestdata.json index b9a199daab6..53f6d575e16 100644 --- a/tests/wpt/tests/url/resources/urltestdata.json +++ b/tests/wpt/tests/url/resources/urltestdata.json @@ -1781,6 +1781,76 @@ "search": "", "hash": "" }, + { + "input": "file:///w|m", + "base": null, + "href": "file:///w|m", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/w|m", + "search": "", + "hash": "" + }, + { + "input": "file:///w||m", + "base": null, + "href": "file:///w||m", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/w||m", + "search": "", + "hash": "" + }, + { + "input": "file:///w|/m", + "base": null, + "href": "file:///w:/m", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/w:/m", + "search": "", + "hash": "" + }, + { + "input": "file:C|/m/", + "base": null, + "href": "file:///C:/m/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C:/m/", + "search": "", + "hash": "" + }, + { + "input": "file:C||/m/", + "base": null, + "href": "file:///C||/m/", + "protocol": "file:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "/C||/m/", + "search": "", + "hash": "" + }, "# Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js", { "input": "http://example.com/././foo", diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/__init__.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/__init__.py index 51b3c64b42d..91899eb50df 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/__init__.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/__init__.py @@ -14,7 +14,7 @@ def assert_browsing_context( info, context, children=None, - is_root=True, + parent_expected=True, parent=None, url=None, user_context="default", @@ -34,7 +34,7 @@ def assert_browsing_context( if context is not None: assert info["context"] == context - if is_root: + if parent_expected: if parent is None: # For a top-level browsing context there is no parent assert info["parent"] is None diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py index 6b7fd8b2be1..8511495594b 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/reference_context.py @@ -32,7 +32,7 @@ async def test_reference_context(bidi_session, value): contexts[0], new_context["context"], children=None, - is_root=True, + parent_expected=True, parent=None, url="about:blank", ) diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/type.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/type.py index cc6d7e51ab6..17703d60123 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/type.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/type.py @@ -35,7 +35,7 @@ async def test_type(bidi_session, top_context, type_hint): contexts[0], new_context["context"], children=None, - is_root=True, + parent_expected=True, parent=None, url="about:blank", ) diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/user_context.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/user_context.py index 51406262fc8..490188c265d 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/user_context.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/create/user_context.py @@ -26,7 +26,7 @@ async def test_user_context(bidi_session, type_hint, create_user_context): contexts[1], new_context["context"], children=None, - is_root=True, + parent_expected=True, parent=None, url="about:blank", user_context=user_context, diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py index 81c664740c1..4fd220ed8fc 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/frames.py @@ -36,7 +36,7 @@ async def test_multiple_frames( child1_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=test_page, ) @@ -47,7 +47,7 @@ async def test_multiple_frames( child2_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=test_page2, ) @@ -84,7 +84,7 @@ async def test_cross_origin( child1_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=test_page_cross_origin, ) @@ -158,7 +158,7 @@ async def test_user_context( child1_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=iframe_url_1, user_context=user_context_id, @@ -174,7 +174,7 @@ async def test_user_context( child2_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=iframe_url_2, user_context=user_context_id, diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py index ca1d0edfa1b..b855b8e7eba 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/max_depth.py @@ -40,7 +40,7 @@ async def test_null( child1_info, context=None, children=1, - is_root=False, + parent_expected=False, parent=None, url=test_page_same_origin_frame, ) @@ -51,7 +51,7 @@ async def test_null( child2_info, context=None, children=0, - is_root=False, + parent_expected=False, parent=None, url=test_page, ) @@ -114,7 +114,7 @@ async def test_top_level_and_one_child( child1_info, context=None, children=None, - is_root=False, + parent_expected=False, parent=None, url=test_page_same_origin_frame, ) diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py index 74d11c60033..40e9f8ac93b 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/get_tree/root.py @@ -93,7 +93,7 @@ async def test_child_context( child1_info, context=None, children=1, - is_root=False, + parent_expected=False, parent=None, url=test_page_same_origin_frame, ) diff --git a/tests/wpt/tests/webdriver/tests/bidi/integration/cookies_with_network_events.py b/tests/wpt/tests/webdriver/tests/bidi/integration/cookies_with_network_events.py index e7fddbb1c47..16ca358e23b 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/integration/cookies_with_network_events.py +++ b/tests/wpt/tests/webdriver/tests/bidi/integration/cookies_with_network_events.py @@ -115,6 +115,13 @@ async def test_fetch( # Clean up cookies in case some other tests failed before cleaning up. await bidi_session.storage.delete_cookies() + # Navigate away from about:blank to make sure document.cookies can be used. + await bidi_session.browsing_context.navigate( + context=new_tab["context"], + url=url("/webdriver/tests/bidi/support/empty.html"), + wait="complete" + ) + cookie_name = "foo" cookie_value = "bar" # Add `Access-Control-Allow-Origin` header for cross-origin request to work. diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/cookies.py b/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/cookies.py index eb79529ab1c..5f594c4b029 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/cookies.py +++ b/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/cookies.py @@ -4,7 +4,7 @@ from webdriver.bidi.modules.network import CookieHeader, Header, NetworkStringVa from webdriver.bidi.modules.script import ContextTarget from ... import recursive_compare -from .. import assert_response_event, RESPONSE_COMPLETED_EVENT +from .. import RESPONSE_COMPLETED_EVENT pytestmark = pytest.mark.asyncio @@ -26,7 +26,15 @@ async def test_modify_cookies( top_context, document_cookies, modified_cookies, + url ): + # Navigate away from about:blank to make sure document.cookies can be used. + await bidi_session.browsing_context.navigate( + context=top_context["context"], + url=url("/webdriver/tests/bidi/support/empty.html"), + wait="complete" + ) + expression = "" for name, value in document_cookies.items(): expression += f"document.cookie = '{name}={value}';" diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/headers.py b/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/headers.py index 11106c77208..1a7acfcde00 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/headers.py +++ b/tests/wpt/tests/webdriver/tests/bidi/network/continue_request/headers.py @@ -44,7 +44,15 @@ async def test_override_cookies( wait_for_event, bidi_session, top_context, + url ): + # Navigate away from about:blank to make sure document.cookies can be used. + await bidi_session.browsing_context.navigate( + context=top_context["context"], + url=url("/webdriver/tests/bidi/support/empty.html"), + wait="complete" + ) + await bidi_session.script.evaluate( expression="document.cookie = 'foo=bar';", target=ContextTarget(top_context["context"]), diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/support/serviceworker.html b/tests/wpt/tests/webdriver/tests/bidi/network/support/serviceworker.html index c7ca7448d05..a1f50e731b2 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/network/support/serviceworker.html +++ b/tests/wpt/tests/webdriver/tests/bidi/network/support/serviceworker.html @@ -23,7 +23,7 @@ } } - addEventListener("beforeunload", (event) => { + addEventListener("pagehide", (event) => { swRegistration?.unregister(); }); </script> diff --git a/tests/wpt/tests/webnn/conformance_tests/add.https.any.js b/tests/wpt/tests/webnn/conformance_tests/add.https.any.js new file mode 100644 index 00000000000..60fef8e0b4a --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/add.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('add', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/buffer.https.any.js b/tests/wpt/tests/webnn/conformance_tests/buffer.https.any.js index 9b4c43198b4..bc838ee7680 100644 --- a/tests/wpt/tests/webnn/conformance_tests/buffer.https.any.js +++ b/tests/wpt/tests/webnn/conformance_tests/buffer.https.any.js @@ -3,6 +3,7 @@ // META: variant=?cpu // META: variant=?gpu // META: variant=?npu +// META: script=../resources/utils_validation.js // META: script=../resources/utils.js // META: timeout=long @@ -11,7 +12,18 @@ // https://webmachinelearning.github.io/webnn/#api-mlbuffer if (navigator.ml) { - testCreateWebNNBuffer('create', 4); + testCreateWebNNBuffer('create', {dataType: 'float16', dimensions: [2, 3]}); + testCreateWebNNBuffer('create', {dataType: 'float32', dimensions: [1, 5]}); + testCreateWebNNBuffer('create', {dataType: 'int32', dimensions: [4]}); + testCreateWebNNBuffer('create', {dataType: 'uint8', dimensions: [3, 2, 4]}); + + testCreateWebNNBufferFails( + 'createFailsEmptyDimension', {dataType: 'int32', dimensions: [2, 0, 3]}); + testCreateWebNNBufferFails('createFailsTooLarge', { + dataType: 'int32', + dimensions: [kMaxUnsignedLong, kMaxUnsignedLong, kMaxUnsignedLong] + }); + testDestroyWebNNBuffer('destroyTwice'); testReadWebNNBuffer('read'); testWriteWebNNBuffer('write'); diff --git a/tests/wpt/tests/webnn/conformance_tests/div.https.any.js b/tests/wpt/tests/webnn/conformance_tests/div.https.any.js new file mode 100644 index 00000000000..65438e6c519 --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/div.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('div', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/elementwise_binary.https.any.js b/tests/wpt/tests/webnn/conformance_tests/elementwise_binary.https.any.js deleted file mode 100644 index 0124077773d..00000000000 --- a/tests/wpt/tests/webnn/conformance_tests/elementwise_binary.https.any.js +++ /dev/null @@ -1,15 +0,0 @@ -// META: title=test WebNN API element-wise binary operations -// META: global=window,dedicatedworker -// META: variant=?cpu -// META: variant=?gpu -// META: variant=?npu -// META: script=../resources/utils.js -// META: timeout=long - -'use strict'; - -// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary - -runWebNNConformanceTests( - ['add', 'sub', 'mul', 'div', 'max', 'min', 'pow'], - buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/gelu.https.any.js b/tests/wpt/tests/webnn/conformance_tests/gelu.https.any.js new file mode 100644 index 00000000000..67287c3ad3f --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/gelu.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API gelu operation +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-gelu + +runWebNNConformanceTests('gelu', buildOperationWithSingleInput); diff --git a/tests/wpt/tests/webnn/conformance_tests/max.https.any.js b/tests/wpt/tests/webnn/conformance_tests/max.https.any.js new file mode 100644 index 00000000000..2281f3ed0a1 --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/max.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('max', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/min.https.any.js b/tests/wpt/tests/webnn/conformance_tests/min.https.any.js new file mode 100644 index 00000000000..5d12a1d7e2c --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/min.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('min', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/mul.https.any.js b/tests/wpt/tests/webnn/conformance_tests/mul.https.any.js new file mode 100644 index 00000000000..94ead346449 --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/mul.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('mul', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/pow.https.any.js b/tests/wpt/tests/webnn/conformance_tests/pow.https.any.js new file mode 100644 index 00000000000..c988e92d917 --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/pow.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('pow', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/conformance_tests/softmax.https.any.js b/tests/wpt/tests/webnn/conformance_tests/softmax.https.any.js index 75893eb34cb..20c050d7bd8 100644 --- a/tests/wpt/tests/webnn/conformance_tests/softmax.https.any.js +++ b/tests/wpt/tests/webnn/conformance_tests/softmax.https.any.js @@ -10,4 +10,4 @@ // https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-softmax -runWebNNConformanceTests('softmax', buildOperationWithSingleInput); +runWebNNConformanceTests('softmax', buildSoftmax); diff --git a/tests/wpt/tests/webnn/conformance_tests/sub.https.any.js b/tests/wpt/tests/webnn/conformance_tests/sub.https.any.js new file mode 100644 index 00000000000..367780e1147 --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/sub.https.any.js @@ -0,0 +1,13 @@ +// META: title=test WebNN API element-wise binary operations +// META: global=window,dedicatedworker +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlgraphbuilder-binary + +runWebNNConformanceTests('sub', buildOperationWithTwoInputs); diff --git a/tests/wpt/tests/webnn/idlharness.https.any.js b/tests/wpt/tests/webnn/idlharness.https.any.js index 93e88294ef1..c2f579a2b4f 100644 --- a/tests/wpt/tests/webnn/idlharness.https.any.js +++ b/tests/wpt/tests/webnn/idlharness.https.any.js @@ -19,26 +19,26 @@ idl_test( } idl_array.add_objects({ - NavigatorML: ['navigator'], ML: ['navigator.ml'], MLContext: ['context'], - MLOperand: ['input', 'filter', 'output'], + MLOperand: ['input', 'constant', 'output'], MLActivation: ['relu'], MLGraphBuilder: ['builder'], MLGraph: ['graph'] }); self.context = await navigator.ml.createContext(); - self.builder = new MLGraphBuilder(self.context); self.input = - builder.input('input', {dataType: 'float32', dimensions: [1, 1, 5, 5]}); - self.filter = builder.constant( - {dataType: 'float32', dimensions: [1, 1, 3, 3]}, - new Float32Array(9).fill(1)); + builder.input('input', {dataType: 'float32', dimensions: [2, 3]}); + self.constant = builder.constant( + {dataType: 'float32', dimensions: [2, 3]}, + new Float32Array(2 * 3).fill(1)); + + // Create an activation which won't be used in the graph. self.relu = builder.relu(); - self.output = - builder.conv2d(input, filter, {activation: relu, inputLayout: "nchw"}); + + self.output = builder.add(input, constant); self.graph = await builder.build({output}); } diff --git a/tests/wpt/tests/webnn/resources/test_data/gelu.json b/tests/wpt/tests/webnn/resources/test_data/gelu.json new file mode 100644 index 00000000000..41422797591 --- /dev/null +++ b/tests/wpt/tests/webnn/resources/test_data/gelu.json @@ -0,0 +1,704 @@ +{ + "tests": [ + { + "name": "gelu float32 0D scalar", + "inputs": { + "input": { + "shape": [], + "data": [ + -0.044885843992233276 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [], + "data": [ + -0.021639423444867134 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 0D scalar", + "inputs": { + "input": { + "shape": [], + "data": [ + -0.044891357421875 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [], + "data": [ + -0.021636962890625 + ], + "type": "float16" + } + }, + { + "name": "gelu float32 1D tensor", + "inputs": { + "input": { + "shape": [24], + "data": [ + 0.878292441368103, + -0.09706497937440872, + 0.1367187649011612, + 0.46406492590904236, + -0.26635801792144775, + -0.8252315521240234, + 0.8530909419059753, + 0.3846154808998108, + 0.6772316694259644, + -0.4811072051525116, + 0.2983909249305725, + 0.6777864098548889, + -0.526228129863739, + 0.3497541546821594, + -0.12918996810913086, + 0.5853934288024902, + -0.8950720429420471, + 0.028302494436502457, + -0.09901237487792969, + -0.8838679790496826, + -0.596120297908783, + 0.31863871216773987, + 0.4794037640094757, + -0.06489315629005432 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [24], + "data": [ + 0.7115113139152527, + -0.0447796992957592, + 0.07579325884580612, + 0.3149605691432953, + -0.10520657151937485, + -0.16885890066623688, + 0.6851989030838013, + 0.24989959597587585, + 0.508513331413269, + -0.1516546905040741, + 0.18419598042964935, + 0.509049117565155, + -0.15753419697284698, + 0.22270187735557556, + -0.05795508995652199, + 0.42198580503463745, + -0.1659233123064041, + 0.014470770955085754, + -0.04560155048966408, + -0.1665063202381134, + -0.1642593890428543, + 0.19914908707141876, + 0.3279957175254822, + -0.030767757445573807 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 1D tensor", + "inputs": { + "input": { + "shape": [24], + "data": [ + 0.87841796875, + -0.0970458984375, + 0.13671875, + 0.464111328125, + -0.266357421875, + -0.8251953125, + 0.85302734375, + 0.384521484375, + 0.67724609375, + -0.481201171875, + 0.29833984375, + 0.677734375, + -0.5263671875, + 0.349853515625, + -0.129150390625, + 0.58544921875, + -0.89501953125, + 0.0283050537109375, + -0.0989990234375, + -0.8837890625, + -0.59619140625, + 0.318603515625, + 0.4794921875, + -0.06488037109375 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [24], + "data": [ + 0.71142578125, + -0.044769287109375, + 0.0758056640625, + 0.31494140625, + -0.105224609375, + -0.1688232421875, + 0.68505859375, + 0.2498779296875, + 0.50830078125, + -0.151611328125, + 0.1842041015625, + 0.5087890625, + -0.1575927734375, + 0.2227783203125, + -0.057952880859375, + 0.422119140625, + -0.1658935546875, + 0.01447296142578125, + -0.04559326171875, + -0.16650390625, + -0.164306640625, + 0.1990966796875, + 0.328125, + -0.03076171875 + ], + "type": "float16" + } + }, + { + "name": "gelu float32 2D tensor", + "inputs": { + "input": { + "shape": [4, 6], + "data": [ + 0.878292441368103, + -0.09706497937440872, + 0.1367187649011612, + 0.46406492590904236, + -0.26635801792144775, + -0.8252315521240234, + 0.8530909419059753, + 0.3846154808998108, + 0.6772316694259644, + -0.4811072051525116, + 0.2983909249305725, + 0.6777864098548889, + -0.526228129863739, + 0.3497541546821594, + -0.12918996810913086, + 0.5853934288024902, + -0.8950720429420471, + 0.028302494436502457, + -0.09901237487792969, + -0.8838679790496826, + -0.596120297908783, + 0.31863871216773987, + 0.4794037640094757, + -0.06489315629005432 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [4, 6], + "data": [ + 0.7115113139152527, + -0.0447796992957592, + 0.07579325884580612, + 0.3149605691432953, + -0.10520657151937485, + -0.16885890066623688, + 0.6851989030838013, + 0.24989959597587585, + 0.508513331413269, + -0.1516546905040741, + 0.18419598042964935, + 0.509049117565155, + -0.15753419697284698, + 0.22270187735557556, + -0.05795508995652199, + 0.42198580503463745, + -0.1659233123064041, + 0.014470770955085754, + -0.04560155048966408, + -0.1665063202381134, + -0.1642593890428543, + 0.19914908707141876, + 0.3279957175254822, + -0.030767757445573807 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 2D tensor", + "inputs": { + "input": { + "shape": [4, 6], + "data": [ + 0.87841796875, + -0.0970458984375, + 0.13671875, + 0.464111328125, + -0.266357421875, + -0.8251953125, + 0.85302734375, + 0.384521484375, + 0.67724609375, + -0.481201171875, + 0.29833984375, + 0.677734375, + -0.5263671875, + 0.349853515625, + -0.129150390625, + 0.58544921875, + -0.89501953125, + 0.0283050537109375, + -0.0989990234375, + -0.8837890625, + -0.59619140625, + 0.318603515625, + 0.4794921875, + -0.06488037109375 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [4, 6], + "data": [ + 0.71142578125, + -0.044769287109375, + 0.0758056640625, + 0.31494140625, + -0.105224609375, + -0.1688232421875, + 0.68505859375, + 0.2498779296875, + 0.50830078125, + -0.151611328125, + 0.1842041015625, + 0.5087890625, + -0.1575927734375, + 0.2227783203125, + -0.057952880859375, + 0.422119140625, + -0.1658935546875, + 0.01447296142578125, + -0.04559326171875, + -0.16650390625, + -0.164306640625, + 0.1990966796875, + 0.328125, + -0.03076171875 + ], + "type": "float16" + } + }, + { + "name": "gelu float32 3D tensor", + "inputs": { + "input": { + "shape": [2, 3, 4], + "data": [ + 0.878292441368103, + -0.09706497937440872, + 0.1367187649011612, + 0.46406492590904236, + -0.26635801792144775, + -0.8252315521240234, + 0.8530909419059753, + 0.3846154808998108, + 0.6772316694259644, + -0.4811072051525116, + 0.2983909249305725, + 0.6777864098548889, + -0.526228129863739, + 0.3497541546821594, + -0.12918996810913086, + 0.5853934288024902, + -0.8950720429420471, + 0.028302494436502457, + -0.09901237487792969, + -0.8838679790496826, + -0.596120297908783, + 0.31863871216773987, + 0.4794037640094757, + -0.06489315629005432 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [2, 3, 4], + "data": [ + 0.7115113139152527, + -0.0447796992957592, + 0.07579325884580612, + 0.3149605691432953, + -0.10520657151937485, + -0.16885890066623688, + 0.6851989030838013, + 0.24989959597587585, + 0.508513331413269, + -0.1516546905040741, + 0.18419598042964935, + 0.509049117565155, + -0.15753419697284698, + 0.22270187735557556, + -0.05795508995652199, + 0.42198580503463745, + -0.1659233123064041, + 0.014470770955085754, + -0.04560155048966408, + -0.1665063202381134, + -0.1642593890428543, + 0.19914908707141876, + 0.3279957175254822, + -0.030767757445573807 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 3D tensor", + "inputs": { + "input": { + "shape": [2, 3, 4], + "data": [ + 0.87841796875, + -0.0970458984375, + 0.13671875, + 0.464111328125, + -0.266357421875, + -0.8251953125, + 0.85302734375, + 0.384521484375, + 0.67724609375, + -0.481201171875, + 0.29833984375, + 0.677734375, + -0.5263671875, + 0.349853515625, + -0.129150390625, + 0.58544921875, + -0.89501953125, + 0.0283050537109375, + -0.0989990234375, + -0.8837890625, + -0.59619140625, + 0.318603515625, + 0.4794921875, + -0.06488037109375 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [2, 3, 4], + "data": [ + 0.71142578125, + -0.044769287109375, + 0.0758056640625, + 0.31494140625, + -0.105224609375, + -0.1688232421875, + 0.68505859375, + 0.2498779296875, + 0.50830078125, + -0.151611328125, + 0.1842041015625, + 0.5087890625, + -0.1575927734375, + 0.2227783203125, + -0.057952880859375, + 0.422119140625, + -0.1658935546875, + 0.01447296142578125, + -0.04559326171875, + -0.16650390625, + -0.164306640625, + 0.1990966796875, + 0.328125, + -0.03076171875 + ], + "type": "float16" + } + }, + { + "name": "gelu float32 4D tensor", + "inputs": { + "input": { + "shape": [2, 2, 2, 3], + "data": [ + 0.878292441368103, + -0.09706497937440872, + 0.1367187649011612, + 0.46406492590904236, + -0.26635801792144775, + -0.8252315521240234, + 0.8530909419059753, + 0.3846154808998108, + 0.6772316694259644, + -0.4811072051525116, + 0.2983909249305725, + 0.6777864098548889, + -0.526228129863739, + 0.3497541546821594, + -0.12918996810913086, + 0.5853934288024902, + -0.8950720429420471, + 0.028302494436502457, + -0.09901237487792969, + -0.8838679790496826, + -0.596120297908783, + 0.31863871216773987, + 0.4794037640094757, + -0.06489315629005432 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [2, 2, 2, 3], + "data": [ + 0.7115113139152527, + -0.0447796992957592, + 0.07579325884580612, + 0.3149605691432953, + -0.10520657151937485, + -0.16885890066623688, + 0.6851989030838013, + 0.24989959597587585, + 0.508513331413269, + -0.1516546905040741, + 0.18419598042964935, + 0.509049117565155, + -0.15753419697284698, + 0.22270187735557556, + -0.05795508995652199, + 0.42198580503463745, + -0.1659233123064041, + 0.014470770955085754, + -0.04560155048966408, + -0.1665063202381134, + -0.1642593890428543, + 0.19914908707141876, + 0.3279957175254822, + -0.030767757445573807 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 4D tensor", + "inputs": { + "input": { + "shape": [2, 2, 2, 3], + "data": [ + 0.87841796875, + -0.0970458984375, + 0.13671875, + 0.464111328125, + -0.266357421875, + -0.8251953125, + 0.85302734375, + 0.384521484375, + 0.67724609375, + -0.481201171875, + 0.29833984375, + 0.677734375, + -0.5263671875, + 0.349853515625, + -0.129150390625, + 0.58544921875, + -0.89501953125, + 0.0283050537109375, + -0.0989990234375, + -0.8837890625, + -0.59619140625, + 0.318603515625, + 0.4794921875, + -0.06488037109375 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [2, 2, 2, 3], + "data": [ + 0.71142578125, + -0.044769287109375, + 0.0758056640625, + 0.31494140625, + -0.105224609375, + -0.1688232421875, + 0.68505859375, + 0.2498779296875, + 0.50830078125, + -0.151611328125, + 0.1842041015625, + 0.5087890625, + -0.1575927734375, + 0.2227783203125, + -0.057952880859375, + 0.422119140625, + -0.1658935546875, + 0.01447296142578125, + -0.04559326171875, + -0.16650390625, + -0.164306640625, + 0.1990966796875, + 0.328125, + -0.03076171875 + ], + "type": "float16" + } + }, + { + "name": "gelu float32 5D tensor", + "inputs": { + "input": { + "shape": [2, 1, 4, 1, 3], + "data": [ + 0.878292441368103, + -0.09706497937440872, + 0.1367187649011612, + 0.46406492590904236, + -0.26635801792144775, + -0.8252315521240234, + 0.8530909419059753, + 0.3846154808998108, + 0.6772316694259644, + -0.4811072051525116, + 0.2983909249305725, + 0.6777864098548889, + -0.526228129863739, + 0.3497541546821594, + -0.12918996810913086, + 0.5853934288024902, + -0.8950720429420471, + 0.028302494436502457, + -0.09901237487792969, + -0.8838679790496826, + -0.596120297908783, + 0.31863871216773987, + 0.4794037640094757, + -0.06489315629005432 + ], + "type": "float32" + } + }, + "expected": { + "name": "output", + "shape": [2, 1, 4, 1, 3], + "data": [ + 0.7115113139152527, + -0.0447796992957592, + 0.07579325884580612, + 0.3149605691432953, + -0.10520657151937485, + -0.16885890066623688, + 0.6851989030838013, + 0.24989959597587585, + 0.508513331413269, + -0.1516546905040741, + 0.18419598042964935, + 0.509049117565155, + -0.15753419697284698, + 0.22270187735557556, + -0.05795508995652199, + 0.42198580503463745, + -0.1659233123064041, + 0.014470770955085754, + -0.04560155048966408, + -0.1665063202381134, + -0.1642593890428543, + 0.19914908707141876, + 0.3279957175254822, + -0.030767757445573807 + ], + "type": "float32" + } + }, + { + "name": "gelu float16 5D tensor", + "inputs": { + "input": { + "shape": [2, 1, 4, 1, 3], + "data": [ + 0.87841796875, + -0.0970458984375, + 0.13671875, + 0.464111328125, + -0.266357421875, + -0.8251953125, + 0.85302734375, + 0.384521484375, + 0.67724609375, + -0.481201171875, + 0.29833984375, + 0.677734375, + -0.5263671875, + 0.349853515625, + -0.129150390625, + 0.58544921875, + -0.89501953125, + 0.0283050537109375, + -0.0989990234375, + -0.8837890625, + -0.59619140625, + 0.318603515625, + 0.4794921875, + -0.06488037109375 + ], + "type": "float16" + } + }, + "expected": { + "name": "output", + "shape": [2, 1, 4, 1, 3], + "data": [ + 0.71142578125, + -0.044769287109375, + 0.0758056640625, + 0.31494140625, + -0.105224609375, + -0.1688232421875, + 0.68505859375, + 0.2498779296875, + 0.50830078125, + -0.151611328125, + 0.1842041015625, + 0.5087890625, + -0.1575927734375, + 0.2227783203125, + -0.057952880859375, + 0.422119140625, + -0.1658935546875, + 0.01447296142578125, + -0.04559326171875, + -0.16650390625, + -0.164306640625, + 0.1990966796875, + 0.328125, + -0.03076171875 + ], + "type": "float16" + } + } + ] +}
\ No newline at end of file diff --git a/tests/wpt/tests/webnn/resources/test_data/softmax.json b/tests/wpt/tests/webnn/resources/test_data/softmax.json index ebb12114fc6..cbe868ac1de 100644 --- a/tests/wpt/tests/webnn/resources/test_data/softmax.json +++ b/tests/wpt/tests/webnn/resources/test_data/softmax.json @@ -198,6 +198,93 @@ ], "type": "float32" } + }, + { + "name": "softmax float32 3D constant tensor", + "inputs": { + "x": { + "shape": [1, 3, 4], + "data": [ + 0.4301910996437073, + 0.5471914410591125, + -1.1637765169143677, + 0.18390046060085297, + 0.583903968334198, + 0.17356790602207184, + 0.5397239923477173, + -0.9535139799118042, + -0.5920282602310181, + -0.17344485223293304, + 0.14395014941692352, + -0.37920907139778137 + ], + "type": "float32", + "constant": true + } + }, + "axis": 1, + "expected": { + "name": "output", + "shape": [1, 3, 4], + "data": [ + 0.39589041471481323, + 0.45983806252479553, + 0.09812675416469574, + 0.529077410697937, + 0.4616699814796448, + 0.31647709012031555, + 0.5390242338180542, + 0.16964708268642426, + 0.142439603805542, + 0.22368484735488892, + 0.36284899711608887, + 0.3012755215167999 + ], + "type": "float32" + } + }, + { + "name": "softmax float32 4D tensor", + "inputs": { + "x": { + "shape": [3, 4, 1, 1], + "data": [ + 0.4301910996437073, + 0.5471914410591125, + -1.1637765169143677, + 0.18390046060085297, + 0.583903968334198, + 0.17356790602207184, + 0.5397239923477173, + -0.9535139799118042, + -0.5920282602310181, + -0.17344485223293304, + 0.14395014941692352, + -0.37920907139778137 + ], + "type": "float32" + } + }, + "axis": 1, + "expected": { + "name": "output", + "shape": [3, 4, 1, 1], + "data": [ + 0.3216537833213806, + 0.3615773916244507, + 0.06533370912075043, + 0.25143513083457947, + 0.35271573066711426, + 0.23400123417377472, + 0.33747196197509766, + 0.07581108063459396, + 0.17110128700733185, + 0.26004093885421753, + 0.3571779429912567, + 0.2116798311471939 + ], + "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 3cfcf579a50..b8a017fb44b 100644 --- a/tests/wpt/tests/webnn/resources/utils.js +++ b/tests/wpt/tests/webnn/resources/utils.js @@ -57,10 +57,33 @@ const getTypedArrayData = (type, size, data) => { return outData; }; +const bytesPerDataType = (dataType) => { + if (dataType === 'int8' || dataType === 'uint8') { + return 1; + } else if (dataType === 'float16') { + return 2; + } else if ( + dataType === 'float32' || dataType === 'int32' || dataType === 'uint32') { + return 4; + } else if (dataType === 'int64' || dataType === 'uint64') { + return 8; + } +}; + const sizeOfShape = (array) => { return array.reduce((accumulator, currentValue) => accumulator * currentValue, 1); }; +const sizeOfDescriptor = (descriptor) => { + return descriptor.dimensions.reduce( + (accumulator, currentValue) => accumulator * currentValue, + bytesPerDataType(descriptor.dataType)); +}; + +const getDescriptorFromBuffer = (buffer) => { + return {dataType: buffer.dataType, dimensions: buffer.shape}; +}; + /** * Get tests resources from test data JSON file of specified operation name. * @param {String} operationName - An operation name @@ -380,6 +403,7 @@ const PrecisionMetrics = { elu: {ULP: {float32: 18, float16: 18}}, expand: {ULP: {float32: 0, float16: 0}}, gather: {ULP: {float32: 0, float16: 0}}, + gelu: {ULP: {float32: 18, float16: 18}}, gemm: {ULP: {float32: getGemmPrecisionTolerance, float16: getGemmPrecisionTolerance}}, instanceNormalization: {ULP: {float32: 840, float16: 8400}}, hardSigmoid: {ULP: {float32: 2, float16: 2}}, @@ -548,15 +572,39 @@ const checkResults = (operationName, namedOutputOperands, outputs, resources) => // the outputs of split() or gru() is a sequence for (let operandName in namedOutputOperands) { const suboutputResource = getNamedResource(expected, operandName); - assert_array_equals(namedOutputOperands[operandName].shape(), suboutputResource.shape ?? []); outputData = outputs[operandName]; + // If data is scalar and shape is not, it means it's expecting to be + // filled by the scalar value. Also limit the array size so it doesn't + // timeout. + if (typeof (suboutputResource.data) === 'number' && + suboutputResource.shape && sizeOfShape(suboutputResource.shape) > 1) { + const size = Math.min( + kMaximumIndexToValidate, sizeOfShape(suboutputResource.shape)); + suboutputResource.data = [ + new Array(size).fill(suboutputResource.data), suboutputResource.type + ]; + outputData = outputData.subarray(0, kMaximumIndexToValidate); + } + assert_array_equals( + namedOutputOperands[operandName].shape(), + suboutputResource.shape ?? []); tolerance = getPrecisonTolerance(operationName, metricType, resources); doAssert(operationName, outputData, suboutputResource.data, tolerance, suboutputResource.type, metricType) } } else { assert_array_equals(namedOutputOperands[expected.name].shape(), expected.shape ?? []); outputData = outputs[expected.name]; + // If data is scalar and shape is not, it means it's expecting to be filled + // by the scalar value. Also limit the array size so it doesn't timeout. + if (typeof (expected.data) === 'number' && expected.shape && + sizeOfShape(expected.shape) > 1) { + const size = + Math.min(kMaximumIndexToValidate, sizeOfShape(expected.shape)); + expected.data = new Array(size).fill(expected.data); + outputData = outputData.subarray(0, kMaximumIndexToValidate); + } expectedData = expected.data; + operandType = expected.type; tolerance = getPrecisonTolerance(operationName, metricType, resources); doAssert(operationName, outputData, expectedData, tolerance, operandType, metricType) @@ -785,6 +833,20 @@ const buildSlice = (operationName, builder, resources) => { return namedOutputOperand; }; +const buildSoftmax = (operationName, builder, resources) => { + // MLOperand softmax(MLOperand input, [EnforceRange] unsigned long axis); + const namedOutputOperand = {}; + const inputOperand = createSingleInputOperand(builder, resources); + if (resources.axis !== undefined) { + // invoke builder.softmax(input, axis) + namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand, resources.axis); + } else { + // invoke builder.softmax(input) + namedOutputOperand[resources.expected.name] = builder[operationName](inputOperand); + } + return namedOutputOperand; +}; + const buildSplit = (operationName, builder, resources) => { // sequence<MLOperand> split(MLOperand input, // (unsigned long or sequence<unsigned long>) splits, @@ -880,7 +942,9 @@ const contextOptions = kContextOptionsForVariant[variant]; */ const isMLBufferSupported = (ml_context) => { - return (createBuffer(ml_context, 4) !== undefined); + return ( + createBuffer(ml_context, {dataType: 'int32', dimensions: [2, 2]}) !== + undefined); } /** @@ -1030,14 +1094,19 @@ const toHalf = (value) => { /** * WebNN buffer creation. * @param {MLContext} context - the context used to create the buffer. - * @param {Number} bufferSize - Size of the buffer to create, in bytes. + * @param {MLBufferDescriptor} bufferDescriptor - intended specs of the buffer. * @returns {MLBuffer} the created buffer. */ -const createBuffer = (context, bufferSize) => { +const createBuffer = (context, bufferDescriptor) => { let buffer; try { - buffer = context.createBuffer({size: bufferSize}); - assert_equals(buffer.size, bufferSize); + buffer = context.createBuffer(bufferDescriptor); + assert_equals( + buffer.dataType, bufferDescriptor.dataType, + 'buffer data types do not match'); + assert_array_equals( + buffer.shape, bufferDescriptor.dimensions, + 'buffer shapes do not match'); } catch (e) { assert_true(e instanceof DOMException); assert_equals(e.name, "NotSupportedError"); @@ -1061,8 +1130,8 @@ const testDestroyWebNNBuffer = (testName) => { } assert_implements( supported, `Unable to create context for ${variant} variant`); - buffer = createBuffer(context, 4); - }); + buffer = createBuffer(context, {dataType: 'int32', dimensions: [2, 3]}); + }); promise_test(async () => { // MLBuffer is not supported for this deviceType. if (buffer === undefined) { @@ -1076,9 +1145,9 @@ const testDestroyWebNNBuffer = (testName) => { /** * WebNN create buffer test. * @param {String} testName - The name of the test operation. - * @param {Number} bufferSize - Size of the buffer to create, in bytes. + * @param {MLBufferDescriptor} bufferDescriptor - The intended buffer specs. */ -const testCreateWebNNBuffer = (testName, bufferSize) => { +const testCreateWebNNBuffer = (testName, bufferDescriptor) => { let context; promise_setup(async () => { @@ -1092,11 +1161,35 @@ const testCreateWebNNBuffer = (testName, bufferSize) => { supported, `Unable to create context for ${variant} variant`); }); promise_test(async () => { - createBuffer(context, bufferSize); - }, `${testName} / ${bufferSize}`); + createBuffer(context, bufferDescriptor); + }, `${testName} / ${bufferDescriptor.dataType}`); }; /** + * Same as above, but expect creating the buffer to fail. + * @param {String} testName - The name of the test operation. + * @param {MLBufferDescriptor} bufferDescriptor - The intended buffer specs. + */ +const testCreateWebNNBufferFails = (testName, bufferDescriptor) => { + let context; + + promise_setup(async () => { + let supported = false; + try { + context = await navigator.ml.createContext(contextOptions); + supported = true; + } catch (e) { + } + assert_implements( + supported, `Unable to create context for ${variant} variant`); + }); + promise_test(async () => { + assert_throws_js(TypeError, () => context.createBuffer(bufferDescriptor)); + }, `${testName} / ${bufferDescriptor.dataType}`); +}; + + +/** * Asserts the buffer data in MLBuffer matches expected. * @param {MLContext} ml_context - The context used to create the buffer. * @param {MLBuffer} ml_buffer - The buffer to read and compare data. @@ -1127,46 +1220,48 @@ const testWriteWebNNBuffer = (testName) => { }); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + const descriptor = {dataType: 'int32', dimensions: [1]}; + let ml_buffer = createBuffer(ml_context, descriptor); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } - let array_buffer = new ArrayBuffer(ml_buffer.size); + const bufferByteLength = sizeOfDescriptor(descriptor); + let array_buffer = new ArrayBuffer(bufferByteLength); // Writing with a size that goes past that source buffer length. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 0, - /*srcSize=*/ ml_buffer.size + 1)); + /*srcSize=*/ bufferByteLength + 1)); assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 3, - /*srcSize=*/ 4)); + /*srcSize=*/ bufferByteLength)); // Writing with a source offset that is out of range of the source size. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), - /*srcOffset=*/ ml_buffer.size + 1)); + /*srcOffset=*/ bufferByteLength + 1)); // Writing with a source offset that is out of range of implicit copy size. assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), - /*srcOffset=*/ ml_buffer.size + 1, /*srcSize=*/ undefined)); + /*srcOffset=*/ bufferByteLength + 1, /*srcSize=*/ undefined)); assert_throws_js( TypeError, () => ml_context.writeBuffer( ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ undefined, - /*srcSize=*/ ml_buffer.size + 1)); + /*srcSize=*/ bufferByteLength + 1)); assert_throws_js( TypeError, @@ -1175,7 +1270,8 @@ const testWriteWebNNBuffer = (testName) => { }, `${testName} / error`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + const descriptor = {dataType: 'int32', dimensions: [2, 2]}; + let ml_buffer = createBuffer(ml_context, descriptor); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1187,19 +1283,20 @@ const testWriteWebNNBuffer = (testName) => { assert_throws_dom( 'InvalidStateError', - () => - ml_context.writeBuffer(ml_buffer, new Uint8Array(ml_buffer.size))); + () => ml_context.writeBuffer( + ml_buffer, new Uint8Array(sizeOfDescriptor(descriptor)))); }, `${testName} / destroy`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + const descriptor = {dataType: 'int32', dimensions: [2, 2]}; + let ml_buffer = createBuffer(ml_context, descriptor); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { return; } - const array_buffer = new ArrayBuffer(ml_buffer.size); + const array_buffer = new ArrayBuffer(sizeOfDescriptor(descriptor)); const detached_buffer = array_buffer.transfer(); assert_true(array_buffer.detached, 'array buffer should be detached.'); @@ -1207,7 +1304,8 @@ const testWriteWebNNBuffer = (testName) => { }, `${testName} / detached`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + const bufferDescriptor = {dataType: 'int32', dimensions: [2, 3]}; + let ml_buffer = createBuffer(ml_context, bufferDescriptor); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1215,9 +1313,10 @@ const testWriteWebNNBuffer = (testName) => { } let another_ml_context = await navigator.ml.createContext(contextOptions); - let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); + let another_ml_buffer = createBuffer(another_ml_context, bufferDescriptor); - let input_data = new Uint8Array(ml_buffer.size).fill(0xAA); + let input_data = + new Uint8Array(sizeOfDescriptor(bufferDescriptor)).fill(0xAA); assert_throws_js( TypeError, () => ml_context.writeBuffer(another_ml_buffer, input_data)); assert_throws_js( @@ -1243,7 +1342,8 @@ const testReadWebNNBuffer = (testName) => { }); promise_test(async t => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [2, 2]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1258,7 +1358,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / destroy`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1276,7 +1377,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / full_size`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1296,7 +1398,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / src_offset_only`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1315,7 +1418,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / zero_write`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1335,7 +1439,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / src_offset_and_size`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1355,7 +1460,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / larger_src_data`); promise_test(async () => { - let ml_buffer = createBuffer(ml_context, 4); + let ml_buffer = + createBuffer(ml_context, {dataType: 'int32', dimensions: [1]}); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1373,7 +1479,8 @@ const testReadWebNNBuffer = (testName) => { }, `${testName} / no_src_offset`); promise_test(async t => { - let ml_buffer = createBuffer(ml_context, 4); + const bufferDescriptor = {dataType: 'int32', dimensions: [2, 3]}; + let ml_buffer = createBuffer(ml_context, bufferDescriptor); // MLBuffer was unsupported for the deviceType. if (ml_buffer === undefined) { @@ -1381,7 +1488,7 @@ const testReadWebNNBuffer = (testName) => { } let another_ml_context = await navigator.ml.createContext(contextOptions); - let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); + let another_ml_buffer = createBuffer(another_ml_context, bufferDescriptor); await promise_rejects_js( t, TypeError, ml_context.readBuffer(another_ml_buffer)); @@ -1411,26 +1518,24 @@ const testDispatchWebNNBuffer = (testName) => { supported, `Unable to create context for ${variant} variant`); // Construct a simple graph: A = B + C, with two outputs. const builder = new MLGraphBuilder(ml_context); - const operandType = {dataType: 'float32', dimensions: shape}; - const lhs_operand = builder.input('lhs', operandType); - const rhs_operand = builder.input('rhs', operandType); + const descriptor = {dataType: 'float32', dimensions: shape}; + const lhs_operand = builder.input('lhs', descriptor); + const rhs_operand = builder.input('rhs', descriptor); const output_1_operand = builder.add(lhs_operand, rhs_operand); const output_2_operand = builder.add(lhs_operand, rhs_operand); ml_graph = await builder.build( {'output1': output_1_operand, 'output2': output_2_operand}); - const ml_buffer_size = - TypedArrayDict['float32'].BYTES_PER_ELEMENT * sizeOfShape(shape); // MLBuffer was unsupported for the deviceType. if (!isMLBufferSupported(ml_context)) { return; } inputs = { - 'lhs': ml_context.createBuffer({size: ml_buffer_size}), - 'rhs': ml_context.createBuffer({size: ml_buffer_size}), + 'lhs': ml_context.createBuffer(descriptor), + 'rhs': ml_context.createBuffer(descriptor), }; outputs = { - 'output1': ml_context.createBuffer({size: ml_buffer_size}), - 'output2': ml_context.createBuffer({size: ml_buffer_size}), + 'output1': ml_context.createBuffer(descriptor), + 'output2': ml_context.createBuffer(descriptor), }; }); @@ -1450,16 +1555,16 @@ const testDispatchWebNNBuffer = (testName) => { TypeError, () => ml_context.dispatch( ml_graph, { - 'lhs': - another_ml_context.createBuffer({size: inputs['lhs'].size()}), + 'lhs': another_ml_context.createBuffer( + getDescriptorFromBuffer(inputs['lhs'])), 'rhs': inputs['rhs'], }, outputs)); // Test the wrong context being used for outputs. assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, { - 'output1': - another_ml_context.createBuffer({size: outputs['output1'].size()}), + 'output1': another_ml_context.createBuffer( + getDescriptorFromBuffer(outputs['output1'])), 'output2': outputs['output2'], })); }, `${testName} / context_mismatch`); @@ -1470,15 +1575,80 @@ const testDispatchWebNNBuffer = (testName) => { return; } - // Control case, valid size. + // Control case, valid buffers. + ml_context.dispatch(ml_graph, inputs, outputs); + + // Input is a different shape. + assert_throws_js( + TypeError, + () => ml_context.dispatch( + ml_graph, { + 'lhs': ml_context.createBuffer({ + dataType: inputs['lhs'].dataType, + // Input rank is too high. + dimensions: inputs['lhs'].shape.concat([2]) + }), + 'rhs': inputs['rhs'], + }, + outputs)); + + assert_throws_js( + TypeError, + () => ml_context.dispatch( + ml_graph, { + 'lhs': inputs['lhs'], + 'rhs': ml_context.createBuffer({ + dataType: inputs['rhs'].dataType, + // Input rank is too low. + dimensions: inputs['rhs'].shape.slice(1) + }), + }, + outputs)); + + // Output is a different shape. Dimension value is too large. + let output1WrongShape = [...outputs['output1'].shape]; + output1WrongShape[0] += 2; + assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, { + 'output1': ml_context.createBuffer({ + dataType: outputs['output1'].dataType, + dimensions: output1WrongShape + }), + 'output2': outputs['output2'], + })); + + // Output is a different shape. Dimension value is too small. + let output2WrongShape = [...outputs['output2'].shape]; + output2WrongShape[1] -= 1; + assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, { + 'output1': outputs['output1'], + 'output2': ml_context.createBuffer({ + dataType: outputs['output2'].dataType, + dimensions: output2WrongShape + }), + })); + }, `${testName} / invalid shape`); + + promise_test(async () => { + // MLBuffer was unsupported for the deviceType. + if (!isMLBufferSupported(ml_context)) { + return; + } + + // Control case, valid buffers. ml_context.dispatch(ml_graph, inputs, outputs); - // Input is too large. + // Inputs are a different data type. + const inputWrongDataType = 'int32'; + assert_not_equals(inputs['lhs'].dataType, inputWrongDataType); + assert_not_equals(inputs['rhs'].dataType, inputWrongDataType); assert_throws_js( TypeError, () => ml_context.dispatch( ml_graph, { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size() + 1}), + 'lhs': ml_context.createBuffer({ + dataType: inputWrongDataType, + dimensions: inputs['lhs'].shape + }), 'rhs': inputs['rhs'], }, outputs)); @@ -1488,21 +1658,33 @@ const testDispatchWebNNBuffer = (testName) => { () => ml_context.dispatch( ml_graph, { 'lhs': inputs['lhs'], - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size() + 1}), + 'rhs': ml_context.createBuffer({ + dataType: inputWrongDataType, + dimensions: inputs['rhs'].shape + }), }, outputs)); - // Output is too large. + // Outputs are a different data type. + const outputWrongDataType = 'int32'; + assert_not_equals(outputs['output1'].dataType, outputWrongDataType); + assert_not_equals(outputs['output2'].dataType, outputWrongDataType); assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, { - 'output1': ml_context.createBuffer({size: outputs['output1'].size() + 1}), + 'output1': ml_context.createBuffer({ + dataType: outputWrongDataType, + dimensions: outputs['output1'].shape + }), 'output2': outputs['output2'], })); assert_throws_js(TypeError, () => ml_context.dispatch(ml_graph, inputs, { 'output1': outputs['output1'], - 'output2': ml_context.createBuffer({size: outputs['output2'].size() + 1}), + 'output2': ml_context.createBuffer({ + dataType: outputWrongDataType, + dimensions: outputs['output2'].shape + }), })); - }, `${testName} / invalid_size`); + }, `${testName} / invalid data type`); promise_test(async () => { // MLBuffer was unsupported for the deviceType. @@ -1562,8 +1744,8 @@ const testDispatchWebNNBuffer = (testName) => { ml_graph, { 'lhs': inputs['lhs'], 'rhs': inputs['rhs'], - 'a_different_input_name': - ml_context.createBuffer({size: inputs['rhs'].size()}), + 'a_different_input_name': ml_context.createBuffer( + getDescriptorFromBuffer(inputs['rhs'])), }, outputs)); @@ -1577,7 +1759,7 @@ const testDispatchWebNNBuffer = (testName) => { 'output1': outputs['output1'], 'output2': outputs['output2'], 'a_different_output_name': - ml_context.createBuffer({size: outputs['output2'].size()}), + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), })); }, `${testName} / invalid_name`); @@ -1634,18 +1816,22 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatch_inputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatch_1_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; const dispatch_2_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1684,18 +1870,20 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatch_1_inputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatch_2_inputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatch_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1731,13 +1919,15 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatch_inputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatch_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1766,18 +1956,22 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatch_inputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatch_1_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; const dispatch_2_outputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1828,11 +2022,14 @@ const testDispatchWebNNBuffer = (testName) => { const graph = await builder.build({'output': builder.sub(lhsOperand, rhsOperand)}); - const lhsBuffer = ml_context.createBuffer({size: inputs['lhs'].size}); - const rhsBuffer = ml_context.createBuffer({size: inputs['rhs'].size}); + const lhsBuffer = + ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])); + const rhsBuffer = + ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])); const dispatchOutputs = { - 'output': ml_context.createBuffer({size: outputs['output1'].size}) + 'output': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])) }; // Initialize inputs @@ -1873,14 +2070,14 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatchInputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const outputBuffer1 = - ml_context.createBuffer({size: outputs['output1'].size}); + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])); const outputBuffer2 = - ml_context.createBuffer({size: outputs['output2'].size}); + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])); // Initialize inputs const inputData1 = @@ -1902,7 +2099,8 @@ const testDispatchWebNNBuffer = (testName) => { ml_context.dispatch(ml_graph, dispatchInputs, { 'output1': outputBuffer1, - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }); // Ensure the last dispatch() did not modify the original second output @@ -1919,13 +2117,15 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatchInputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatchOutputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1939,7 +2139,8 @@ const testDispatchWebNNBuffer = (testName) => { // Check destroyed input buffers cannot be re-used in subsequent dispatches. dispatchInputs['lhs'].destroy(); - dispatchInputs['lhs'] = ml_context.createBuffer({size: inputs['lhs'].size}); + dispatchInputs['lhs'] = + ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])); const newInputData = new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0); @@ -1953,7 +2154,8 @@ const testDispatchWebNNBuffer = (testName) => { new Float32Array(sizeOfShape(shape)).fill(3)); dispatchInputs['rhs'].destroy(); - dispatchInputs['rhs'] = ml_context.createBuffer({size: inputs['rhs'].size}); + dispatchInputs['rhs'] = + ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])); ml_context.writeBuffer(dispatchInputs['rhs'], newInputData); // Output = LHS + RHS = 2 + 2 = 4 @@ -1971,13 +2173,15 @@ const testDispatchWebNNBuffer = (testName) => { } const dispatchInputs = { - 'lhs': ml_context.createBuffer({size: inputs['lhs'].size}), - 'rhs': ml_context.createBuffer({size: inputs['rhs'].size}), + 'lhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['lhs'])), + 'rhs': ml_context.createBuffer(getDescriptorFromBuffer(inputs['rhs'])), }; const dispatchOutputs = { - 'output1': ml_context.createBuffer({size: outputs['output1'].size}), - 'output2': ml_context.createBuffer({size: outputs['output2'].size}), + 'output1': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output1'])), + 'output2': + ml_context.createBuffer(getDescriptorFromBuffer(outputs['output2'])), }; // Initialize inputs @@ -1992,8 +2196,8 @@ const testDispatchWebNNBuffer = (testName) => { // Check destroyed output buffers cannot be re-used in subsequent // dispatches. dispatchOutputs['output1'].destroy(); - dispatchOutputs['output1'] = - ml_context.createBuffer({size: outputs['output1'].size}); + dispatchOutputs['output1'] = ml_context.createBuffer( + getDescriptorFromBuffer(outputs['output1'])); const newInputData = new TypedArrayDict['float32'](sizeOfShape(shape)).fill(2.0); 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 126fa90e167..ce1394802ed 100644 --- a/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js @@ -6,8 +6,7 @@ validateInputFromAnotherBuilder('clamp'); -validateUnaryOperation( - 'clamp', allWebNNOperandDataTypes, /*alsoBuildActivation=*/ true); +validateUnaryOperation('clamp', allWebNNOperandDataTypes); promise_test(async t => { const options = {minValue: 1.0, maxValue: 3.0}; @@ -28,11 +27,6 @@ promise_test(async t => { }, '[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]}); @@ -46,15 +40,3 @@ promise_test(async t => { 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/gru.https.any.js b/tests/wpt/tests/webnn/validation_tests/gru.https.any.js index 50d39d297a3..b1b576e96d0 100644 --- a/tests/wpt/tests/webnn/validation_tests/gru.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/gru.https.any.js @@ -430,8 +430,8 @@ multi_builder_test(async (t, builder, otherBuilder) => { }, '[gru] throw if initialHiddenState option is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { - const activation = builder.clamp(); - const activationFromOtherBuilder = otherBuilder.clamp(); + const activation = builder.relu(); + const activationFromOtherBuilder = otherBuilder.relu(); const options = {activations: [activation, activationFromOtherBuilder]}; const input = builder.input('input', kExampleInputDescriptor); diff --git a/tests/wpt/tests/webnn/validation_tests/gruCell.https.any.js b/tests/wpt/tests/webnn/validation_tests/gruCell.https.any.js index 3cd9d32b073..0465928d514 100644 --- a/tests/wpt/tests/webnn/validation_tests/gruCell.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/gruCell.https.any.js @@ -454,8 +454,8 @@ multi_builder_test(async (t, builder, otherBuilder) => { }, '[gruCell] throw if recurrentBias option is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { - const activation = builder.clamp(); - const activationFromOtherBuilder = otherBuilder.clamp(); + const activation = builder.relu(); + const activationFromOtherBuilder = otherBuilder.relu(); const options = {activations: [activation, activationFromOtherBuilder]}; const input = builder.input('input', kExampleInputDescriptor); diff --git a/tests/wpt/tests/webnn/validation_tests/lstm.https.any.js b/tests/wpt/tests/webnn/validation_tests/lstm.https.any.js index 18d609798ce..c7d341a9d5a 100644 --- a/tests/wpt/tests/webnn/validation_tests/lstm.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/lstm.https.any.js @@ -456,8 +456,8 @@ multi_builder_test(async (t, builder, otherBuilder) => { }, '[lstm] throw if initialCellState option is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { - const activation = builder.clamp(); - const activationFromOtherBuilder = otherBuilder.clamp(); + const activation = builder.relu(); + const activationFromOtherBuilder = otherBuilder.relu(); const options = {activations: [activation, activationFromOtherBuilder]}; const input = builder.input('input', kExampleInputDescriptor); diff --git a/tests/wpt/tests/webnn/validation_tests/lstmCell.https.any.js b/tests/wpt/tests/webnn/validation_tests/lstmCell.https.any.js index c3769c828d4..d4031b07b24 100644 --- a/tests/wpt/tests/webnn/validation_tests/lstmCell.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/lstmCell.https.any.js @@ -187,8 +187,8 @@ multi_builder_test(async (t, builder, otherBuilder) => { }, '[lstmCell] throw if peepholeWeight option is from another builder'); multi_builder_test(async (t, builder, otherBuilder) => { - const activation = builder.clamp(); - const activationFromOtherBuilder = otherBuilder.clamp(); + const activation = builder.relu(); + const activationFromOtherBuilder = otherBuilder.relu(); const options = {activations: [activation, activationFromOtherBuilder]}; const input = builder.input('input', kExampleInputDescriptor); diff --git a/tests/wpt/tests/webnn/validation_tests/softmax.https.any.js b/tests/wpt/tests/webnn/validation_tests/softmax.https.any.js index 68891b27d88..a75878307f6 100644 --- a/tests/wpt/tests/webnn/validation_tests/softmax.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/softmax.https.any.js @@ -4,4 +4,97 @@ 'use strict'; -validateInputFromAnotherBuilder('softmax'); +const tests_without_axis = [ + { + name: '[softmax] Test building Softmax with float32 input without axis.', + input: { dataType: 'float32', dimensions: [4, 3] }, + output: { dataType: 'float32', dimensions: [4, 3] } + }, + { + name: '[softmax] Test building Softmax with float16 input without axis.', + input: { dataType: 'float16', dimensions: [3, 5] }, + output: { dataType: 'float16', dimensions: [3, 5] } + }, + { + name: '[softmax] Throw if the input is not a non-floating point data.', + input: { dataType: 'int32', dimensions: [3, 2] } + }, + { + name: '[softmax] Throw if the input dimensions is not 2.', + input: { dataType: 'float32', dimensions: [1, 4, 3] } + } +]; + +tests_without_axis.forEach(test => + promise_test(async t => { + let input = builder.input( + `input`, { dataType: test.input.dataType, dimensions: test.input.dimensions } + ); + if (test.output) { + const output = builder.softmax(input); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js(TypeError, () => builder.softmax(input)); + } + }, test.name) +); + +multi_builder_test(async (t, builder, otherBuilder) => { + const operandDescriptor = { dataType: 'float32', dimensions: [2, 3] }; + const inputFromOtherBuilder = otherBuilder.input('input', operandDescriptor); + + assert_throws_js( + TypeError, + () => builder.softmax(inputFromOtherBuilder)); +}, '[softmax without axis] throw if any input is from another builder'); + +const tests = [ + { + name: '[softmax] Test building Softmax with float32 input.', + input: { dataType: 'float32', dimensions: [4, 4, 3] }, + axis: 1, + output: { dataType: 'float32', dimensions: [4, 4, 3] } + }, + { + name: '[softmax] Test building Softmax with float16 input.', + input: { dataType: 'float16', dimensions: [3, 1, 5, 2] }, + axis: 2, + output: { dataType: 'float16', dimensions: [3, 1, 5, 2] } + }, + { + name: '[softmax] Throw if the input is not a non-floating-point data.', + input: { dataType: 'int32', dimensions: [3, 1, 5, 2] }, + axis: 3 + }, + { + name: '[softmax] Throw if the axis is greater than input rank - 1.', + input: { dataType: 'float16', dimensions: [3, 1, 5, 2] }, + axis: 4 + } +]; + +tests.forEach(test => + promise_test(async t => { + let input = builder.input( + `input`, { dataType: test.input.dataType, dimensions: test.input.dimensions } + ); + if (test.output) { + const output = builder.softmax(input, test.axis); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js(TypeError, () => builder.softmax(input, test.axis)); + } + }, test.name) +); + +multi_builder_test(async (t, builder, otherBuilder) => { + const operandDescriptor = { dataType: 'float32', dimensions: [1, 2, 3] }; + const inputFromOtherBuilder = otherBuilder.input('input', operandDescriptor); + const axis = 1; + + assert_throws_js( + TypeError, + () => builder.softmax(inputFromOtherBuilder, axis)); +}, '[softmax] throw if any input is from another builder'); diff --git a/tests/wpt/tests/webrtc/RTCRtpParameters-codecs.html b/tests/wpt/tests/webrtc/RTCRtpParameters-codecs.html index 97f519a45e4..933677f792b 100644 --- a/tests/wpt/tests/webrtc/RTCRtpParameters-codecs.html +++ b/tests/wpt/tests/webrtc/RTCRtpParameters-codecs.html @@ -1,5 +1,6 @@ <!doctype html> <meta charset=utf-8> +<meta name="timeout" content="long"> <title>RTCRtpParameters codecs</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> @@ -438,8 +439,7 @@ }); // SDP with unusual payload types and fmtp, and an unknown codec - const audioSdp = ` -v=0 + const audioSdp = `v=0 o=- 166855176514521964 2 IN IP4 127.0.0.1 s=- t=0 0 @@ -460,8 +460,7 @@ a=fmtp:111 maxaveragebitrate=20001;unknownparam=foo `; // SDP with unusual payload types and fmtp, and an unknown codec - const videoSdp = ` -v=0 + const videoSdp = `v=0 o=- 1878890426675213188 2 IN IP4 127.0.0.1 s=- t=0 0 diff --git a/tests/wpt/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html b/tests/wpt/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html index d728ec5a9c9..005286497cb 100644 --- a/tests/wpt/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html +++ b/tests/wpt/tests/webrtc/RTCRtpReceiver-audio-jitterBufferTarget-stats.https.html @@ -5,7 +5,7 @@ <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/webrtc/RTCPeerConnection-helper.js"></script> -<script src="/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats-helper.js"></script> +<script src="/webrtc/RTCRtpReceiver-jitterBufferTarget-stats-helper.js"></script> <body> <script> 'use strict' diff --git a/tests/wpt/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html b/tests/wpt/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html index 022dbe70c50..d69d7f51990 100644 --- a/tests/wpt/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html +++ b/tests/wpt/tests/webrtc/RTCRtpReceiver-video-jitterBufferTarget-stats.html @@ -5,7 +5,7 @@ <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/webrtc/RTCPeerConnection-helper.js"></script> -<script src="/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats-helper.js"></script> +<script src="/webrtc/RTCRtpReceiver-jitterBufferTarget-stats-helper.js"></script> <body> <script> 'use strict' diff --git a/tests/wpt/tests/webrtc/idlharness.https.window.js b/tests/wpt/tests/webrtc/idlharness.https.window.js index 98685f1cd12..58e696f73c1 100644 --- a/tests/wpt/tests/webrtc/idlharness.https.window.js +++ b/tests/wpt/tests/webrtc/idlharness.https.window.js @@ -47,32 +47,37 @@ function asyncInitCertificate() { // Asynchronously generate instances of // RTCSctpTransport, RTCDtlsTransport, // and RTCIceTransport -function asyncInitTransports() { - const pc = new RTCPeerConnection(); - pc.createDataChannel('test'); - - // setting answer description initializes pc.sctp - return pc.createOffer() - .then(offer => - pc.setLocalDescription(offer) - .then(() => generateAnswer(offer))) - .then(answer => pc.setRemoteDescription(answer)) - .then(() => { - const sctpTransport = pc.sctp; - assert_true(sctpTransport instanceof RTCSctpTransport, - 'Expect pc.sctp to be instance of RTCSctpTransport'); - idlTestObjects.sctpTransport = sctpTransport; - - const dtlsTransport = sctpTransport.transport; - assert_true(dtlsTransport instanceof RTCDtlsTransport, - 'Expect sctpTransport.transport to be instance of RTCDtlsTransport'); - idlTestObjects.dtlsTransport = dtlsTransport; - - const iceTransport = dtlsTransport.iceTransport; - assert_true(iceTransport instanceof RTCIceTransport, - 'Expect sctpTransport.transport to be instance of RTCDtlsTransport'); - idlTestObjects.iceTransport = iceTransport; - }); +async function asyncInitTransports() { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + pc1.createDataChannel('test'); + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + const sctpTransport = pc1.sctp; + assert_true(sctpTransport instanceof RTCSctpTransport, + 'Expect pc1.sctp to be instance of RTCSctpTransport'); + idlTestObjects.sctpTransport = sctpTransport; + + const dtlsTransport = sctpTransport.transport; + assert_true(dtlsTransport instanceof RTCDtlsTransport, + 'Expect dtlsTransport.transport to be instance of RTCDtlsTransport'); + idlTestObjects.dtlsTransport = dtlsTransport; + + const iceTransport = dtlsTransport.iceTransport; + assert_true(iceTransport instanceof RTCIceTransport, + 'Expect iceTransport.transport to be instance of RTCIceTransport'); + idlTestObjects.iceTransport = iceTransport; + await waitForIceStateChange(pc1, ['connected']); + + assert_not_equals(iceTransport.state, "new", 'Expect iceTransport.state to be not new'); + assert_not_equals(iceTransport.state, "closed", 'Expect iceTransport.state to be not closed'); + + const iceCandidatePair = iceTransport.getSelectedCandidatePair(); + + assert_not_equals(iceCandidatePair, null, 'Expect iceTransport selected pair to be not null'); + assert_true(iceCandidatePair instanceof RTCIceCandidatePair, + 'Expect iceTransport.getSelectedCandidatePair() to be instance of RTCIceTransport'); + idlTestObjects.iceCandidatePair = iceCandidatePair; } // Asynchoronously generate MediaStreamTrack from getUserMedia @@ -129,6 +134,7 @@ idl_test( RTCSctpTransport: ['idlTestObjects.sctpTransport'], RTCDtlsTransport: ['idlTestObjects.dtlsTransport'], RTCIceTransport: ['idlTestObjects.iceTransport'], + RTCIceCandidatePair: ['idlTestObjects.iceCandidatePair'], MediaStreamTrack: ['idlTestObjects.mediaStreamTrack'], }); /* diff --git a/tests/wpt/tests/webrtc/simulcast/setParameters-maxFramerate.https.html b/tests/wpt/tests/webrtc/simulcast/setParameters-maxFramerate.https.html new file mode 100644 index 00000000000..e404787dbd8 --- /dev/null +++ b/tests/wpt/tests/webrtc/simulcast/setParameters-maxFramerate.https.html @@ -0,0 +1,77 @@ +<!doctype html> +<meta charset=utf-8> +<title>RTCPeerConnection Simulcast Tests - setParameters/maxFramerate</title> +<meta name="timeout" content="long"> +<script src="../third_party/sdp/sdp.js"></script> +<script src="simulcast.js"></script> +<script src="../RTCPeerConnection-helper.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +async function queryReceiverStats(pc) { + const inboundStats = []; + await Promise.all(pc.getReceivers().map(async receiver => { + const receiverStats = await receiver.getStats(); + receiverStats.forEach(stat => { + if (stat.type === 'inbound-rtp') { + inboundStats.push(stat); + } + }); + })); + return inboundStats; +} + +async function statsDelta(pc, t) { + const initialStats = await queryReceiverStats(pc); + await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait more. + const subsequentStats = await queryReceiverStats(pc); + return {initialStats, subsequentStats}; +} + +function calculateFramerate(newStats, oldStats) { + const deltaF = newStats.framesDecoded - oldStats.framesDecoded; + const deltaT = newStats.timestamp - oldStats.timestamp; + return deltaF / deltaT * 1000; +} + +promise_test(async t => { + const expectedFramerates = [15, 10, 5]; + const rids = [0, 1, 2]; + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + + await negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2); + + // Assert that our framerate is bigger than 20, otherwise this test does not assert anything useful. + await new Promise(resolve => t.step_timeout(resolve, 1000)); // Wait for framerate to stabilize. + const defaultStats = await statsDelta(pc2, t); + defaultStats.subsequentStats.forEach((_, idx) => { + const actualFramerate = calculateFramerate(defaultStats.subsequentStats[idx], defaultStats.initialStats[idx]); + assert_greater_than(actualFramerate, 20); + }); + + // Change the framerate on all layers. + const parameters = pc1.getSenders()[0].getParameters(); + parameters.encodings.forEach((e, idx) => { + e.maxFramerate = expectedFramerates[idx]; + }); + await pc1.getSenders()[0].setParameters(parameters); + await new Promise(resolve => t.step_timeout(resolve, 100)); // Wait for the change to propagate to the receiver. + + // Assert the approximate framerate + const newStats = await statsDelta(pc2, t); + + const framerates = []; + newStats.subsequentStats.forEach((_, idx) => { + const expectedFramerate = expectedFramerates[idx]; + const actualFramerate = calculateFramerate(newStats.subsequentStats[idx], newStats.initialStats[idx]); + // Assert that the framerate is at most the expected framerate (plus a tiny bit). + assert_less_than(actualFramerate, expectedFramerate + 0.5); + framerates.push(actualFramerate); + }); + // Assert that the framerates are ordered as configured. + assert_equals(framerates, framerates.sort()); +}, 'Simulcast setParameters maxFramerate reduces the framerate'); +</script> diff --git a/tests/wpt/tests/window-management/multi-screen-window-open-fullscreen.tentative.https.html b/tests/wpt/tests/window-management/multi-screen-window-open-fullscreen.tentative.https.html deleted file mode 100644 index 008c53d4e74..00000000000 --- a/tests/wpt/tests/window-management/multi-screen-window-open-fullscreen.tentative.https.html +++ /dev/null @@ -1,51 +0,0 @@ -<!DOCTYPE html> -<meta name="timeout" content="long"> -<!-- user agents are not required to support open features other than `noopener` - and on some platforms position and size features don't make sense --> -<meta name="flags" content="may"> -<title>Window Management test: Fullscreen popups with window.open()</title> -<link rel="help" href="https://w3c.github.io/window-management/"> -Tests the ability to open a fullscreen popup window on each screen.<br> -The host device must have 2+ screens to test cross-screen fullscreen popups. -<br><br> -<button id="closeButton" onclick="closePopups">Close popups</button><br> -<input id="autoClose" type="checkbox" checked=true>Auto-close popups</input> -<ul id="list"></ul> -<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/helpers.js"></script> -<script> - 'use strict'; - - let popups = []; - function closePopups() { - popups.forEach(p => p.close()); - popups = []; - } - - promise_test(async setUpTest => { - await setUpWindowManagement(setUpTest); - for (const [i, s] of window.screenDetails.screens.entries()) { - const name = `Open a fullscreen popup on '${s.label || i}'`; - await promise_test(async test => { - await buttonClick(test, name); - const popup = await openPopupOnScreen(s, /*assertPlacement=*/false, - /*fullscreen=*/true); - popups.push(popup); - await poll(() => { - return popup.document.fullscreenElement == - popup.document.documentElement - }); - const context = `popup: ${windowLog(popup)}, ${screenLog(screen)}`; - assert_equals(popup.screenLeft, s.availLeft, context) - assert_equals(popup.screenRight, s.availRight, context); - assert_equals(popup.screen.availHeight, s.availHeight, context); - assert_equals(popup.screen.availWidth, s.availWidth, context); - if (autoClose.checked) - closePopups(); - }, name); - } - }, 'Use multi-screen details to open a fullscreen popup window on each screen'); -</script> diff --git a/tests/wpt/tests/window-management/resources/helpers.js b/tests/wpt/tests/window-management/resources/helpers.js index ee77fd8d30d..9a2b90549ba 100644 --- a/tests/wpt/tests/window-management/resources/helpers.js +++ b/tests/wpt/tests/window-management/resources/helpers.js @@ -65,13 +65,10 @@ async function poll(condition, interval = 100, duration = 3000) { } // Open and return a popup on `screen`, optionally asserting placement. -async function openPopupOnScreen(screen, assertPlacement = true, fullscreen = false) { +async function openPopupOnScreen(screen, assertPlacement = true) { const left = screen.availLeft + Math.floor(screen.availWidth / 2) - 150; const top = screen.availTop + Math.floor(screen.availHeight / 2) - 50; let features = `left=${left},top=${top},width=300,height=100`; - if (fullscreen) { - features += ",fullscreen"; - } log(`Opening a popup with features '${features}' on ${screenLog(screen)}`); // Window.open() synchronously returns a Window with estimated screenLeft|Top, // which may be clamped to the opener's screen or incompletely initialized. diff --git a/tests/wpt/tests/workers/shared-worker-options-mismatch.html b/tests/wpt/tests/workers/shared-worker-options-mismatch.html index 604f69a4367..76beda937fb 100644 --- a/tests/wpt/tests/workers/shared-worker-options-mismatch.html +++ b/tests/wpt/tests/workers/shared-worker-options-mismatch.html @@ -8,10 +8,18 @@ const mismatch_test = (testName, firstOptions, secondOptions) => { promise_test(async () => { firstOptions.name = testName; secondOptions.name = testName; - const scriptURL = 'support/empty-worker.js'; + const scriptURL = 'support/SharedWorker-common.js'; const firstWorker = new SharedWorker(scriptURL, firstOptions); const secondWorker = new SharedWorker(scriptURL, secondOptions); - return new Promise(resolve => secondWorker.onerror = resolve); + const result = new Promise((resolve, reject) => { + secondWorker.onerror = resolve; + secondWorker.port.onmessage = () => reject("Worker loaded succesfully"); + }); + secondWorker.port.postMessage("ping"); + await result; + firstWorker.port.postMessage("close"); + secondWorker.port.postMessage("close"); + return result; }, 'Connecting to shared worker with different options should be blocked: ' + testName); }; |